package com.nagarro.client;

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

import com.smartgwt.client.data.Criteria;
import com.smartgwt.client.data.DSCallback;
import com.smartgwt.client.data.DSRequest;
import com.smartgwt.client.data.DataSource;
import com.smartgwt.client.data.DataSourceField;
import com.smartgwt.client.data.Record;
import com.smartgwt.client.data.RecordList;
import com.smartgwt.client.data.ResultSet;
import com.smartgwt.client.data.SortSpecifier;
import com.smartgwt.client.types.DSOperationType;
import com.smartgwt.client.types.FieldAuditMode;
import com.smartgwt.client.types.KeyNames;
import com.smartgwt.client.types.ListGridEditEvent;
import com.smartgwt.client.types.RowEndEditAction;
import com.smartgwt.client.types.SelectionAppearance;
import com.smartgwt.client.types.SelectionStyle;
import com.smartgwt.client.types.SortDirection;
import com.smartgwt.client.util.EventHandler;
import com.smartgwt.client.util.SC;
import com.smartgwt.client.widgets.events.KeyPressEvent;
import com.smartgwt.client.widgets.grid.ListGrid;
import com.smartgwt.client.widgets.grid.ListGridField;
import com.smartgwt.client.widgets.grid.ListGridRecord;
import com.smartgwt.client.widgets.grid.events.CellContextClickHandler;
import com.smartgwt.client.widgets.menu.Menu;
import com.smartgwt.client.widgets.menu.MenuItem;

/**
 * ASSA ABLOY ListGrid implementation. Adds additional functionality on top of the regular ListGrid. All additional functionality has to be enabled with
 * {@code setEnable...()} calls.
 */
public class AAListGrid extends ListGrid {

    // <editor-fold desc="State Fields" defaultstate="collapsed">
    private boolean disableEditing;
    private boolean contextMenuEnabled;

    private boolean             autoSaveEdits;
    private boolean             canDragSelect;
    private boolean             canEdit;
    private boolean             canExpandRecords;
    private boolean             canReorderRecords;
    private boolean             canSort;
    private SelectionStyle      selectionStyle;
    private SelectionAppearance selectionAppearance;
    private Criteria            initialCriteria;
    // </editor-fold>

    // <editor-fold desc="KeyPress Fields" defaultstate="collapsed">
    private boolean enableDeleteKey = true;
    // </editor-fold>

    // <editor-fold desc="Enable Fill Down Fields" defaultstate="collapsed">
    private boolean      enableFillDown;
    private List<String> fillDownFields;
    private MenuItem     fillDownMenuItem;
    // </editor-fold>

    // <editor-fold desc="View Manager Fields" defaultstate="collapsed">
    private boolean enableViewManager;
    // </editor-fold>

    // <editor-fold desc="Undo Action Fields" defaultstate="collapsed">
    private boolean  enableUndoAction;
    private MenuItem undoMenuItem;
    private Long     auditModifier;
    private String   auditAddOperation;
    private String   auditUpdateOperation;
    private String   auditRemoveOperation;

    // </editor-fold>

    public AAListGrid() {
        setDefaults();
    }

    public AAListGrid(final DataSource dataSource) {
        super(dataSource);
        setDefaults();
    }

    /**
     * Sets default ListGrid functionality.
     */
    private void setDefaults() {
        // Multi-select with checkboxes
        setSelectionAppearance(SelectionAppearance.CHECKBOX);
        setSelectionType(SelectionStyle.SIMPLE);

        // Single click edit mode
        setEditEvent(ListGridEditEvent.CLICK);

        // Disable "columns" menu in the header context
        setCanPickFields(false);

        // No action on end, leaves focus in current box
        setListEndEditAction(RowEndEditAction.STOP);

        // Handle key press events
        addKeyPressHandler(this::onKeyPressEvent);

        // Don't show the sort numerals for now
        setShowSortNumerals(false);

        // No multi sort, that enables the "Configure Sort" option which needs to be handled to not show hidden fields
        setCanMultiSort(false);

        // Alternate record style
        setAlternateRecordStyles(true);

        // To Show Hover for clipped data only
        setShowClippedValuesOnHover(true);

    }

    /**
     * Disable editing on this ListGrid.
     */
    private void disableEditing() {
        canEdit = getCanEdit();
        disableEditing = true;
    }

    /**
     * Re-enable editing on this ListGrid. This only resets the canEdit state from the call prior to {@link #disableEditing()}.
     */
    private void enableEditing() {
        disableEditing = false;
        setCanEdit(canEdit);
    }

    // <editor-fold desc="Criteria Methods" defaultstate="collapsed">
    /**
     * Captures the criteria as the initial criteria if it hasn't been set already.
     *
     * @param criteria {@link Criteria}
     */
    private void captureInitialCriteria(final Criteria criteria) {
        if (initialCriteria == null) {
            initialCriteria = criteria;
        }
    }

    /**
     * Capturing {@link Criteria} as the initial criteria if this is the first time it's being set.
     */
    @Override
    public void setCriteria(final Criteria criteria) {
        captureInitialCriteria(criteria);
        super.setCriteria(criteria);
    }

    /**
     * Capturing {@link Criteria} as the initial criteria if this is the first time it's being set.
     */
    @Override
    public void fetchData(final Criteria criteria) {
        captureInitialCriteria(criteria);
        super.fetchData(criteria);
    }

    /**
     * Capturing {@link Criteria} as the initial criteria if this is the first time it's being set.
     */
    @Override
    public void fetchData(final Criteria criteria, final DSCallback dsCallback) {
        captureInitialCriteria(criteria);
        super.fetchData(criteria, dsCallback);
    }

    /**
     * Capturing {@link Criteria} as the initial criteria if this is the first time it's being set.
     */
    @Override
    public void fetchData(final Criteria criteria, final DSCallback dsCallback, final DSRequest dsRequest) {
        captureInitialCriteria(criteria);
        super.fetchData(criteria, dsCallback, dsRequest);
    }

    /**
     * Capturing {@link Criteria} as the initial criteria if this is the first time it's being set.
     */
    @Override
    public void filterData(final Criteria criteria) {
        captureInitialCriteria(criteria);
        super.filterData(criteria);
    }

    /**
     * Capturing {@link Criteria} as the initial criteria if this is the first time it's being set.
     */
    @Override
    public void filterData(final Criteria criteria, final DSCallback dsCallback) {
        captureInitialCriteria(criteria);
        super.filterData(criteria, dsCallback);
    }

    /**
     * Capturing {@link Criteria} as the initial criteria if this is the first time it's being set.
     */
    @Override
    public void filterData(final Criteria criteria, final DSCallback dsCallback, final DSRequest dsRequest) {
        captureInitialCriteria(criteria);
        super.filterData(criteria, dsCallback, dsRequest);
    }
    // </editor-fold>

    @Override
    protected MenuItem[] getHeaderContextMenuItems(final Integer integer) {
        final List<MenuItem> menuItems = Arrays.stream(super.getHeaderContextMenuItems(integer)).collect(Collectors.toList());

        return menuItems.toArray(new MenuItem[0]);
    }

    /**
     * Instead of clearing the criteria, restore the initial criteria if available.
     */
    @Override
    public void clearCriteria() {
        final Criteria criteria = (initialCriteria == null) ? getInitialCriteria() : initialCriteria;
        if (getShowFilterEditor()) {
            filterData(criteria);
        } else {
            setCriteria(criteria);
        }
    }

    /**
     * If a default sort is set, reverts back to that. Otherwise calls regular clearSort().
     */
    @Override
    public void clearSort() {

        super.clearSort();

    }

    @Override
    public Boolean getCanEdit() {
        if (disableEditing) {
            return false;
        } else {
            return super.getCanEdit();
        }
    }

    @Override
    public boolean canEditCell(final int i, final int i1) {
        if (disableEditing) {
            return false;
        } else {
            return super.canEditCell(i, i1);
        }
    }

    /**
     * {@inheritDoc}
     *
     * Prompts before deletion if {@link #getWarnOnRemoval()} is true.
     */
    @Override
    public void removeSelectedData() {
        if ((getSelectedRecords() != null) && (getSelectedRecords().length > 0) && Boolean.TRUE.equals(getWarnOnRemoval())) {
            SC.ask(getWarnOnRemovalMessage(), confirm -> {
                if (Boolean.TRUE.equals(confirm)) {
                    super.removeSelectedData();
                }
            });
        } else {
            super.removeSelectedData();
        }
    }

    /**
     * {@inheritDoc}
     *
     * Prompts before deletion if {@link #getWarnOnRemoval()} is true.
     */
    @Override
    public void removeSelectedData(final DSCallback callback) {
        if ((getSelectedRecords() != null) && (getSelectedRecords().length > 0) && Boolean.TRUE.equals(getWarnOnRemoval())) {
            SC.ask(getWarnOnRemovalMessage(), confirm -> {
                if (Boolean.TRUE.equals(confirm)) {
                    super.removeSelectedData(callback);
                }
            });
        } else {
            super.removeSelectedData(callback);
        }
    }

    /**
     * {@inheritDoc}
     *
     * Prompts before deletion if {@link #getWarnOnRemoval()} is true.
     */
    @Override
    public void removeSelectedData(final DSCallback dsCallback, final DSRequest dsRequest) {
        if ((getSelectedRecords() != null) && (getSelectedRecords().length > 0) && Boolean.TRUE.equals(getWarnOnRemoval())) {
            SC.ask(getWarnOnRemovalMessage(), confirm -> {
                if (Boolean.TRUE.equals(confirm)) {
                    super.removeSelectedData(dsCallback, dsRequest);
                }
            });
        } else {
            super.removeSelectedData(dsCallback, dsRequest);
        }
    }

    /**
     * Similar to the {@link #getFieldNum(String)} by uses all available fields as specified by {@link #getAllFields()} instead of just the visible fields.
     *
     * @param fieldId field name
     * @return field index or -1 if doesnt exist
     */
    public int getAllFieldsNum(final String fieldId) {
        int index = -1;
        final ListGridField[] allFields = getAllFields();
        final int length = allFields.length;
        for (int i = 0; i < length; i++) {
            if (allFields[i].getName().equals(fieldId)) {
                index = i;
                break;
            }
        }
        return index;
    }

    /**
     * Sets the fields in the list grid based on the passed in field array.
     *
     * @param fields Array of field names
     * @return this for chained calls
     */
    public AAListGrid setGridFields(final String... fields) {
        setFields(Arrays.stream(fields).map(ListGridField::new).toArray(ListGridField[]::new));
        return this;
    }

    // <editor-fold desc="Keyboard Action Methods" defaultstate="collapsed">
    /**
     * Key press event handler.
     *
     * @param event {@link KeyPressEvent}
     */
    private void onKeyPressEvent(final KeyPressEvent event) {
        final String key = EventHandler.getKey();
        if (KeyNames.DEL.equals(key)) {
            deleteAction();
        }
        if (event.isCtrlKeyDown() && "Z".equals(key) && enableUndoAction) {
            performUndo();
        }
    }

    /**
     * Deletes the selected data if {@link #getCanEdit()} is true. Prompts the user for confirmation if {@link #getWarnOnRemoval()} is set.
     */
    private void deleteAction() {
        if (enableDeleteKey && Boolean.TRUE.equals(getCanEdit()) && (getSelectedRecords() != null) && (getSelectedRecords().length > 0)) {
            if (Boolean.TRUE.equals(getWarnOnRemoval())) {
                SC.ask(getWarnOnRemovalMessage(), confirm -> {
                    if (Boolean.TRUE.equals(confirm)) {
                        super.removeSelectedData();
                    }
                });
            } else {
                removeSelectedData();
            }
        }
    }
    // </editor-fold>

    // <editor-fold desc="Context Menu Methods" defaultstate="collapsed">
    /**
     * Enables showing the context menu on right click.
     */
    public void setEnableContextMenu() {
        if (!contextMenuEnabled) {
            addCellContextClickHandler(cellContextClickHandler());
            contextMenuEnabled = true;
        }
    }

    /**
     * Gets whether the context menu is enabled.
     *
     * @return boolean
     */
    public boolean getEnableContextMenu() {
        return contextMenuEnabled;
    }

    /**
     * Context handler. Shows the context menu on right-click.
     *
     * @return {@link CellContextClickHandler}
     */
    private CellContextClickHandler cellContextClickHandler() {
        return contextClickEvent -> {
            final MenuItem[] menuItems = getContextMenuItems(contextClickEvent.getRowNum(), contextClickEvent.getColNum());
            if (menuItems.length > 0) {
                final Menu menu = new Menu();
                menu.setItems(menuItems);
                menu.showContextMenu();
            }
            contextClickEvent.cancel();
        };
    }

    /**
     * Gets context menu items based on record, row, and column numbers.
     *
     * @param record {@link ListGridRecord} for the event
     * @param rowNum row number
     * @param colNum column number
     * @return Array of {@link MenuItem}
     */
    private MenuItem[] getContextMenuItems(final int rowNum, final int colNum) {
        final List<MenuItem> menuItems = new ArrayList<>();

        // Fill down menu item
        if (enableFillDown) {
            final MenuItem menuItem = getFillDownMenuItem(rowNum, colNum);
            if (menuItem != null) {
                menuItems.add(menuItem);
            }
        }

        // Undo menu item
        if (enableUndoAction) {
            final MenuItem menuItem = getUndoMenuItem();
            menuItems.add(menuItem);
        }

        return menuItems.toArray(new MenuItem[0]);
    }
    // </editor-fold>

    // <editor-fold desc="KeyPress Methods" defaultstate="collapsed">
    /**
     * Set enabling the default delete key handling.<br>
     * <br>
     * Defaults to {@code true}.
     *
     * @param enable boolean to enable delete key handling
     * @return AAListGrid for chained calls
     */
    public AAListGrid setEnableDeleteKey(final boolean enable) {
        enableDeleteKey = enable;
        return this;
    }

    /**
     * Gets whether the default delete key handling is enabled.
     *
     * @return boolean delete key handling enabled
     */
    public boolean getEnableDeleteKey() {
        return enableDeleteKey;
    }
    // </editor-fold>

    // </editor-fold>

    // <editor-fold desc="Enable Fill Down Methods" defaultstate="collapsed">
    /**
     * Enable the "Fill Value Down" right click menu for certain columns.
     *
     * @param columns array of columns to enable for
     * @return AAListGrid for chained calls
     */
    public AAListGrid setEnableFillDownForColumns(final String... columns) {
        if ((columns == null) || (columns.length == 0)) {
            throw new IllegalArgumentException("Columns are required");
        }
        enableFillDown = true;
        setEnableContextMenu();
        fillDownFields = Arrays.asList(columns);
        return this;
    }

    /**
     * Lazily instantiates the fill down menu item.
     *
     * @param rowNum event row number
     * @param colNum event column number
     * @return {@link MenuItem}
     */
    private MenuItem getFillDownMenuItem(final Integer rowNum, final Integer colNum) {
        final String field = getFieldName(colNum);
        if (fillDownFields.contains(field)) {

            // Instantiate the menu item
            if (fillDownMenuItem == null) {
                fillDownMenuItem = new MenuItem();
                fillDownMenuItem.addClickHandler(menuItemClickEvent -> {
                    // Current cell value
                    final Object value = getRecord(rowNum).getAttribute(getFieldName(colNum));

                    // Get the total rows, but just what's been loaded on the client, not all records
                    final RecordList recordList = isGrouped() ? getOriginalRecordList() : getRecordList();
                    final int totalRows;
                    if (recordList instanceof ResultSet) {
                        totalRows = ((ResultSet) recordList).getAllVisibleRows().getLength();
                    } else {
                        totalRows = recordList.getLength();
                    }

                    // Fill any rows down with the value
                    for (int i = rowNum + 1; i < totalRows; i++) {
                        setEditValue(i, colNum, value);
                    }
                    saveAllEdits();
                });
            }

            // Set the title
            final Object value = getRecord(rowNum).getAttribute(field);
            if (value == null) {
                fillDownMenuItem.setTitle("Cler Value Down");
            } else {
                fillDownMenuItem.setTitle("FIl Value Down");
            }
            return fillDownMenuItem;
        } else {
            return null;
        }
    }
    // </editor-fold>

    /**
     * Gets whether the view manager is enabled for this ListGrid.
     *
     * @return view manager enabled boolean
     */
    public boolean getEnableViewManager() {
        return enableViewManager;
    }

    // </editor-fold>

    // <editor-fold desc="Undo Action Fields" defaultstate="collapsed">
    /**
     * Enables the "UNDO" option in the row context menu for this ListGrid.<br>
     * <br>
     * Defaults to {@code false}.
     *
     * @param enable enable boolean
     * @return AAListGrid for chained calls
     */
    public AAListGrid setEnableUndoAction(final long auditModifier, final String addOperation, final String updateOperation, final String removeOperation) {
        if (((addOperation == null) || (updateOperation == null) || (removeOperation == null))) {
            throw new IllegalArgumentException("Operation IDs are required");
        }
        enableUndoAction = true;
        this.auditModifier = auditModifier;
        auditRemoveOperation = removeOperation;
        auditAddOperation = addOperation;
        auditUpdateOperation = updateOperation;

        setEnableContextMenu();
        DataSource.load("audit_" + getDataSource().getID(), null);

        return this;
    }

    /**
     * Checks whether the "UNDO" option is enabled in the row context menu.<br>
     * <br>
     * Defaults to {@code false}.
     *
     * @return print enabled boolean
     */
    public boolean getEnableUndoAction() {
        return enableUndoAction;
    }

    /**
     * Lazily instantiates the UNDO menu item. This will load the audit DS and get the top 1 audit data for the logged in user. Once the audit data is fetched,
     * it will get the operation type which was performed and accordingly will perform the UNDO operation
     *
     * @return {@link MenuItem}
     */
    private MenuItem getUndoMenuItem() {

        if (undoMenuItem == null) {
            undoMenuItem = new MenuItem();
            undoMenuItem.addClickHandler(menuItemClickEvent -> performUndo());
        }

        undoMenuItem.setTitle("Undo");
        return undoMenuItem;

    }

    private void performUndo() {
        final DataSource gridDataSource = getDataSource();
        final String auditDataSourceId = "audit_" + gridDataSource.getID();
        final String primaryKeyName = gridDataSource.getPrimaryKeyFieldName();
        final Criteria contactCriteria = new Criteria();
        final DSRequest properties = new DSRequest();
        final SortSpecifier sortByversion = new SortSpecifier("audit_revision", SortDirection.DESCENDING);
        final SortSpecifier[] sorting = new SortSpecifier[0];
        sorting[0] = sortByversion;
        properties.setSortBy(sorting);
        // This is to fetch the top two row with highest audit_revision number, which means the latest change done by user
        properties.setStartRow(0);
        properties.setEndRow(2);
        final DataSource auditDS = DataSource.get(auditDataSourceId);
        if (null != auditDS) {
            auditDS.fetchData(contactCriteria, (final com.smartgwt.client.data.DSResponse dsResponse, final Object data, final DSRequest dsRequest) -> {
                if ((dsResponse.getData() != null) && (dsResponse.getData().length > 0)) {
                    final Record currentAuditRecord = dsResponse.getData()[0];
                    final String primaryKeyValInAudit = currentAuditRecord.getAttribute(primaryKeyName);
                    final String operationType = currentAuditRecord.getAttributeAsString("audit_operationType");
                    final DSRequest auditActionProperties = new DSRequest();

                    if (DSOperationType.UPDATE.getValue().equals(operationType) && (dsResponse.getData().length > 1)) {
                        final Record lastAuditRecord = dsResponse.getData()[1];
                        final Record changedRecord = getResultSet().find(primaryKeyName, primaryKeyValInAudit);
                        final int rowNum = getRecordIndex(changedRecord);
                        if (lastAuditRecord.getAttribute(primaryKeyName).equals(primaryKeyValInAudit)) {
                            final Object[] fieldsChangedObj = (Object[]) currentAuditRecord.getAttributeAsObject("audit_changedFields");
                            for (final Object fieldChangedObj : fieldsChangedObj) {
                                final DataSourceField field = gridDataSource.getField(fieldChangedObj.toString());
                                // Skip fields that shouldn't be audited
                                // TODO this has to be revisited once the skipAudit and audit never works file. Ideally when audit never is used, this should
                                // never go to audit table and hence we will not get the field in audit_changedFields
                                if (!FieldAuditMode.NEVER.getValue().equalsIgnoreCase(field.getAttribute("audit"))) {
                                    setEditValue(rowNum, fieldChangedObj.toString(), lastAuditRecord.getAttribute(fieldChangedObj.toString()));
                                }
                            }
                            auditActionProperties.setOperationId(auditUpdateOperation);

                            setUpdateOperation(auditUpdateOperation);
                            saveAllEdits(() -> refreshRow(getRecordIndex(changedRecord)));

                        }

                    } else if (DSOperationType.ADD.getValue().equals(operationType)) {
                        final Record changedRecord = getResultSet().find(primaryKeyName, primaryKeyValInAudit);
                        final int rowNum = getRecordIndex(changedRecord);
                        setRemoveOperation(auditRemoveOperation);
                        markRecordRemoved(rowNum);
                        saveAllEdits(this::refreshData);

                    } else if (DSOperationType.REMOVE.getValue().equals(operationType)) {
                        currentAuditRecord.setAttribute(primaryKeyName, (String) null);
                        auditActionProperties.setOperationId(auditAddOperation);
                        gridDataSource.addData(currentAuditRecord, (response, obj, request) -> refreshData(), auditActionProperties);
                    }
                    // This remove is for the actual Audit entry which was created by User's action. This remove can be considered similar to a Stack Pop
                    // action.
                    auditDS.removeData(currentAuditRecord);

                } else {
                    SC.say("No History");
                }

            }, properties);
        } else {
            SC.say("No DS");
        }

    }

    // </editor-fold>

}
