/**
 * Copyright (c) 2015 Bosch Software Innovations GmbH and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.eclipse.hawkbit.ui.common.table;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.hawkbit.repository.model.NamedEntity;
import org.eclipse.hawkbit.ui.SpPermissionChecker;
import org.eclipse.hawkbit.ui.artifacts.event.UploadArtifactUIEvent;
import org.eclipse.hawkbit.ui.common.ConfirmationDialog;
import org.eclipse.hawkbit.ui.common.ManagementEntityState;
import org.eclipse.hawkbit.ui.common.UserDetailsFormatter;
import org.eclipse.hawkbit.ui.components.RefreshableContainer;
import org.eclipse.hawkbit.ui.components.SPUIComponentProvider;
import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleNoBorderWithIcon;
import org.eclipse.hawkbit.ui.utils.SPDateTimeUtil;
import org.eclipse.hawkbit.ui.utils.SPUIDefinitions;
import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions;
import org.eclipse.hawkbit.ui.utils.TableColumn;
import org.eclipse.hawkbit.ui.utils.UIMessageIdProvider;
import org.eclipse.hawkbit.ui.utils.UINotification;
import org.eclipse.hawkbit.ui.utils.VaadinMessageSource;
import org.vaadin.addons.lazyquerycontainer.LazyQueryContainer;
import org.vaadin.spring.events.EventBus;
import org.vaadin.spring.events.EventBus.UIEventBus;

import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.event.Transferable;
import com.vaadin.event.dd.DragAndDropEvent;
import com.vaadin.event.dd.DropHandler;
import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
import com.vaadin.server.FontAwesome;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Component;
import com.vaadin.ui.DragAndDropWrapper;
import com.vaadin.ui.Table;
import com.vaadin.ui.UI;
import com.vaadin.ui.themes.ValoTheme;

/**
 * Abstract table to handling entity
 *
 * @param <E>
 *            e is the entity class
 */
public abstract class AbstractTable<E extends NamedEntity> extends Table implements RefreshableContainer {

    private static final long serialVersionUID = 1L;

    protected static final String MESSAGE_CONFIRM_DELETE_ENTITY = "message.confirm.delete.entity";

    protected static final String DISTRIBUTIONSET_NOT_EXISTS = "distributionset.not.exists";

    protected static final String TARGETS_NOT_EXISTS = "targets.not.exists";

    protected static final String CAPTION_ENTITY_ASSIGN_ACTION_CONFIRMBOX = "caption.entity.assign.action.confirmbox";

    protected static final String MESSAGE_CONFIRM_ASSIGN_ENTITY = "message.confirm.assign.entity";

    protected static final String MESSAGE_CONFIRM_ASSIGN_MULTIPLE_ENTITIES = "message.confirm.assign.multiple.entities";

    private static final float DEFAULT_COLUMN_NAME_MIN_SIZE = 0.8F;

    private final transient EventBus.UIEventBus eventBus;

    private final VaadinMessageSource i18n;

    private final UINotification notification;

    private final SpPermissionChecker permChecker;

    protected AbstractTable(final UIEventBus eventBus, final VaadinMessageSource i18n,
            final UINotification notification, final SpPermissionChecker permChecker) {
        this.eventBus = eventBus;
        this.i18n = i18n;
        this.notification = notification;
        this.permChecker = permChecker;
        setStyleName("sp-table");
        setSizeFull();
        setImmediate(true);
        setHeight(100.0F, Unit.PERCENTAGE);
        addStyleName(ValoTheme.TABLE_NO_VERTICAL_LINES);
        addStyleName(ValoTheme.TABLE_SMALL);
        setSortEnabled(false);
        setId(getTableId());
        addCustomGeneratedColumns();
        addDeleteColumn();
        setDefault();
        addValueChangeListener(event -> onValueChange());
        setPageLength(SPUIDefinitions.PAGE_SIZE);
        if (doSubscribeToEventBus()) {
            eventBus.subscribe(this);
        }
    }

    /**
     * Subscribes the view to the eventBus. Method has to be overriden (return
     * false) if the view does not contain any listener to avoid Vaadin blowing
     * up our logs with warnings.
     */
    protected boolean doSubscribeToEventBus() {
        return true;
    }

    // can be overriden
    protected boolean hasDeletePermission() {
        return permChecker.hasDeleteRepositoryPermission();
    }

    private void addDeleteColumn() {
        if (hasDeletePermission()) {
            addGeneratedColumn(SPUIDefinitions.DELETE_ENTITY, (source, itemId, columnId) -> createDeleteButton(itemId));
        }
    }

    /**
     * Gets the selected item id or in multiselect mode the selected ids.
     * 
     * @param table
     *            the table to retrieve the selected ID(s)
     * @return the ID(s) which are selected in the table
     */
    @SuppressWarnings("unchecked")
    public static <T> Set<T> getTableValue(final Table table) {
        final Object value = table.getValue();
        Set<T> idsReturn;
        if (value == null) {
            idsReturn = Collections.emptySet();
        } else if (value instanceof Collection) {
            final Collection<T> ids = (Collection<T>) value;
            idsReturn = ids.stream().filter(Objects::nonNull).collect(Collectors.toSet());
        } else {
            final T id = (T) value;
            idsReturn = Collections.singleton(id);
        }
        return idsReturn;
    }

    private void onValueChange() {
        eventBus.publish(this, UploadArtifactUIEvent.HIDE_DROP_HINTS);

        final Set<Long> values = getTableValue(this);

        Long lastId = null;
        if (!values.isEmpty()) {
            lastId = Iterables.getLast(values);
        }
        setManagementEntityStateValues(values, lastId);
        selectEntity(lastId);
        afterEntityIsSelected();
    }

    protected void setManagementEntityStateValues(final Set<Long> values, final Long lastId) {
        final ManagementEntityState managementEntityState = getManagementEntityState();
        if (managementEntityState == null) {
            return;
        }
        managementEntityState.setLastSelectedEntityId(lastId);
        managementEntityState.setSelectedEnitities(values);
    }

    private void setDefault() {
        setSelectable(true);
        setMultiSelect(true);
        setDragMode(TableDragMode.MULTIROW);
        setColumnCollapsingAllowed(false);
        setDropHandler(getTableDropHandler());
    }

    protected void addNewContainerDS() {
        final Container container = createContainer();
        addContainerProperties(container);
        setContainerDataSource(container);
        final int size = container.size();
        if (size == 0) {
            setData(i18n.getMessage(UIMessageIdProvider.MESSAGE_NO_DATA));
        }
    }

    protected void selectRow() {
        if (!isMaximized()) {
            final Object itemIdToSelect = getItemIdToSelect();
            if (itemIdToSelect == null) {
                return;
            }
            setValue(itemIdToSelect);
        }
    }

    /**
     * Select all rows in the table.
     */
    protected void selectAll() {
        if (isMultiSelect()) {
            // only contains the ItemIds of the visible items in the table
            setValue(getItemIds());
        }
    }

    protected void setColumnProperties() {
        final List<TableColumn> columnList = getTableVisibleColumns();
        addDeleteButtonToColumnList(columnList);
        final List<Object> swColumnIds = new ArrayList<>();
        for (final TableColumn column : columnList) {
            setColumnHeader(column.getColumnPropertyId(), column.getColumnHeader());
            setColumnExpandRatio(column.getColumnPropertyId(), column.getExpandRatio());
            swColumnIds.add(column.getColumnPropertyId());
        }
        setVisibleColumns(swColumnIds.toArray());
    }

    private void addDeleteButtonToColumnList(final List<TableColumn> columnList) {
        if (hasDeletePermission()) {
            columnList.add(new TableColumn(SPUIDefinitions.DELETE_ENTITY, i18n.getMessage("header.delete"), 0.0F));
        }
    }

    private void applyMaxTableSettings() {
        setColumnProperties();
        setValue(null);
        setSelectable(false);
        setMultiSelect(false);
        setDragMode(TableDragMode.NONE);
        setColumnCollapsingAllowed(true);
    }

    private void applyMinTableSettings() {
        setDefault();
        setColumnProperties();
        selectRow();
    }

    protected void refreshFilter() {
        addNewContainerDS();
        setColumnProperties();
        selectRow();
    }

    @SuppressWarnings("unchecked")
    protected void updateEntity(final E baseEntity, final Item item) {
        item.getItemProperty(SPUILabelDefinitions.VAR_NAME).setValue(baseEntity.getName());
        item.getItemProperty(SPUILabelDefinitions.VAR_ID).setValue(baseEntity.getId());
        item.getItemProperty(SPUILabelDefinitions.VAR_DESC).setValue(baseEntity.getDescription());
        item.getItemProperty(SPUILabelDefinitions.VAR_CREATED_BY)
                .setValue(UserDetailsFormatter.loadAndFormatCreatedBy(baseEntity));
        item.getItemProperty(SPUILabelDefinitions.VAR_LAST_MODIFIED_BY)
                .setValue(UserDetailsFormatter.loadAndFormatLastModifiedBy(baseEntity));
        item.getItemProperty(SPUILabelDefinitions.VAR_CREATED_DATE)
                .setValue(SPDateTimeUtil.getFormattedDate(baseEntity.getCreatedAt()));
        item.getItemProperty(SPUILabelDefinitions.VAR_LAST_MODIFIED_DATE)
                .setValue(SPDateTimeUtil.getFormattedDate(baseEntity.getLastModifiedAt()));

    }

    protected void onBaseEntityEvent(final BaseUIEntityEvent<E> event) {
        if (BaseEntityEventType.MINIMIZED == event.getEventType()) {
            UI.getCurrent().access(this::applyMinTableSettings);
        } else if (BaseEntityEventType.MAXIMIZED == event.getEventType()) {
            UI.getCurrent().access(this::applyMaxTableSettings);
        } else if (BaseEntityEventType.ADD_ENTITY == event.getEventType()
                || BaseEntityEventType.REMOVE_ENTITY == event.getEventType()) {
            UI.getCurrent().access(this::refreshContainer);
        }
    }

    /**
     * Return the entity which should be deleted by a transferable
     * 
     * @param transferable
     *            the table transferable
     * @return set of entities id which will deleted
     */
    public Set<Long> getSelectedEntitiesByTransferable(final TableTransferable transferable) {
        final Set<Long> selectedEntities = getTableValue(this);
        final Set<Long> ids = new HashSet<>();
        final Long transferableData = (Long) transferable.getData(SPUIDefinitions.ITEMID);
        if (transferableData == null) {
            return ids;
        }

        if (entityToBeDeletedIsSelectedInTable(transferableData, selectedEntities)) {
            ids.addAll(selectedEntities);
        } else {
            ids.add(transferableData);
        }
        return ids;
    }

    /**
     * Finds the entity object of the given entity ID by performing a database
     * search
     * 
     * @param lastSelectedId
     *            ID of the entity
     * @return entity object as Optional
     */
    protected abstract Optional<E> findEntityByTableValue(Long lastSelectedId);

    /**
     * This method is performed after selecting the current entity in the table.
     */
    protected void afterEntityIsSelected() {
        // can be overridden by subclass
    }

    /**
     * Publish the BaseEntityEventType.SELECTED_ENTITY Event with the given
     * entity.
     * 
     * @param selectedLastEntity
     *            entity that was selected in the table
     */
    protected abstract void publishSelectedEntityEvent(final E selectedLastEntity);

    protected void setLastSelectedEntityId(final Long selectedLastEntityId) {
        if (selectedLastEntityId == null) {
            return;
        }
        getManagementEntityState().setLastSelectedEntityId(selectedLastEntityId);
    }

    protected abstract ManagementEntityState getManagementEntityState();

    /**
     * Get Id of the table.
     * 
     * @return Id.
     */
    protected abstract String getTableId();

    /**
     * Create container of the data to be displayed by the table.
     */
    protected abstract Container createContainer();

    /**
     * Add container properties to the container passed in the reference.
     * 
     * @param container
     *            reference of {@link Container}
     */
    protected abstract void addContainerProperties(Container container);

    /**
     * Add any generated columns if required.
     */
    protected void addCustomGeneratedColumns() {
        // can be overriden
    }

    private Object createDeleteButton(final Object itemId) {
        final Button deleteButton = SPUIComponentProvider.getButton("", "", "", "", true, FontAwesome.TRASH_O,
                SPUIButtonStyleNoBorderWithIcon.class);
        final String id = getEntityId(itemId);
        deleteButton.setId("delete.entity." + id);
        deleteButton.setDescription(i18n.getMessage(UIMessageIdProvider.TOOLTIP_DELETE));
        deleteButton.addClickListener(this::addDeleteButtonClickListener);
        return deleteButton;
    }

    private void addDeleteButtonClickListener(final ClickEvent event) {
        openConfirmationWindowDeleteAction(event);
    }

    private void openConfirmationWindowDeleteAction(final ClickEvent event) {
        final List<Long> entitiesToBeDeleted = getEntitiesForDeletion(event);
        final String confirmationQuestion = createConfirmationQuestionForDeletion(entitiesToBeDeleted);
        final ConfirmationDialog confirmDialog = createConfirmationWindowForDeletion(entitiesToBeDeleted,
                confirmationQuestion);
        UI.getCurrent().addWindow(confirmDialog.getWindow());
        confirmDialog.getWindow().bringToFront();
    }

    private List<Long> getEntitiesForDeletion(final ClickEvent event) {
        List<Long> entitiesToBeDeleted;
        final Long id = getDeleteButtonId(event);
        final Set<Long> selectedEntities = getSelectedEntities();
        if (entityToBeDeletedIsSelectedInTable(id, selectedEntities)) {
            entitiesToBeDeleted = selectedEntities.stream().collect(Collectors.toList());
        } else {
            final Table table = getTable(event);
            unselectSelectedEntitiesInTable(selectedEntities, table);
            selectEntityToDeleteInTable(id, table);
            entitiesToBeDeleted = new ArrayList<>();
            entitiesToBeDeleted.add(id);
        }
        return entitiesToBeDeleted;
    }

    private ConfirmationDialog createConfirmationWindowForDeletion(final List<Long> entitiesToBeDeleted,
            final String confirmationQuestion) {
        return new ConfirmationDialog(i18n.getMessage("caption.entity.delete.action.confirmbox", getEntityType()),
                confirmationQuestion, i18n.getMessage(UIMessageIdProvider.BUTTON_OK),
                i18n.getMessage(UIMessageIdProvider.BUTTON_CANCEL), ok -> {
                    if (ok) {
                        handleOkDelete(entitiesToBeDeleted);
                    }
                });
    }

    private String createConfirmationQuestionForDeletion(final List<Long> entitiesToBeDeleted) {
        if (entitiesToBeDeleted.size() == 1) {
            return i18n.getMessage(MESSAGE_CONFIRM_DELETE_ENTITY, getEntityType().toLowerCase(),
                    getDeletedEntityName(entitiesToBeDeleted.get(0)), "");
        } else {
            return i18n.getMessage(MESSAGE_CONFIRM_DELETE_ENTITY, entitiesToBeDeleted.size(),
                    getEntityType().toLowerCase(), "s");
        }
    }

    private static boolean entityToBeDeletedIsSelectedInTable(final Long id, final Set<Long> selectedEntities) {
        return selectedEntities.contains(id);
    }

    private static Table getTable(final ClickEvent event) {
        final Button source = (Button) event.getSource();
        return (Table) source.getParent();
    }

    private static void selectEntityToDeleteInTable(final Long id, final Table table) {
        table.select(id);
    }

    private static void unselectSelectedEntitiesInTable(final Set<Long> selectedEntities, final Table table) {
        selectedEntities.forEach(table::unselect);
    }

    private static long getDeleteButtonId(final ClickEvent event) {
        final String id = event.getButton().getId();
        return Long.parseLong(id.substring(id.lastIndexOf('.') + 1));
    }

    protected abstract String getDeletedEntityName(Long entityId);

    protected abstract void handleOkDelete(List<Long> allEntities);

    protected abstract String getEntityType();

    protected abstract Set<Long> getSelectedEntities();

    protected abstract String getEntityId(Object itemId);

    /**
     * Get Item Id which should be displayed as selected.
     * 
     * @return reference of Item Id of the Row.
     */
    protected abstract Object getItemIdToSelect();

    /**
     * Check if the table is maximized or minimized.
     * 
     * @return true if maximized, otherwise false.
     */
    protected abstract boolean isMaximized();

    /**
     * Based on table state (max/min) columns to be shown are returned.
     * 
     * @return List<TableColumn> list of visible columns
     */
    protected List<TableColumn> getTableVisibleColumns() {
        final List<TableColumn> columnList = new ArrayList<>();
        if (!isMaximized()) {
            columnList.add(new TableColumn(SPUILabelDefinitions.VAR_NAME, i18n.getMessage("header.name"),
                    getColumnNameMinimizedSize()));
            return columnList;
        }
        columnList.add(new TableColumn(SPUILabelDefinitions.VAR_NAME, i18n.getMessage("header.name"), 0.2F));
        columnList.add(new TableColumn(SPUILabelDefinitions.VAR_CREATED_BY, i18n.getMessage("header.createdBy"), 0.1F));
        columnList.add(
                new TableColumn(SPUILabelDefinitions.VAR_CREATED_DATE, i18n.getMessage("header.createdDate"), 0.1F));
        columnList.add(
                new TableColumn(SPUILabelDefinitions.VAR_LAST_MODIFIED_BY, i18n.getMessage("header.modifiedBy"), 0.1F));
        columnList.add(new TableColumn(SPUILabelDefinitions.VAR_LAST_MODIFIED_DATE,
                i18n.getMessage("header.modifiedDate"), 0.1F));
        columnList.add(new TableColumn(SPUILabelDefinitions.VAR_DESC, i18n.getMessage("header.description"), 0.2F));
        setItemDescriptionGenerator((source, itemId, propertyId) -> {
            if (SPUILabelDefinitions.VAR_CREATED_BY.equals(propertyId)) {
                return getItem(itemId).getItemProperty(SPUILabelDefinitions.VAR_CREATED_BY).getValue().toString();
            }
            if (SPUILabelDefinitions.VAR_LAST_MODIFIED_BY.equals(propertyId)) {
                return getItem(itemId).getItemProperty(SPUILabelDefinitions.VAR_LAST_MODIFIED_BY).getValue().toString();
            }
            return null;
        });
        return columnList;
    }

    protected float getColumnNameMinimizedSize() {
        return DEFAULT_COLUMN_NAME_MIN_SIZE;
    }

    private DropHandler getTableDropHandler() {
        return new DropHandler() {
            private static final long serialVersionUID = 1L;

            @Override
            public AcceptCriterion getAcceptCriterion() {
                return getDropAcceptCriterion();
            }

            @Override
            public void drop(final DragAndDropEvent event) {
                if (!isDropValid(event)) {
                    return;
                }
                if (event.getTransferable().getSourceComponent() instanceof Table) {
                    onDropEventFromTable(event);
                } else if (event.getTransferable().getSourceComponent() instanceof DragAndDropWrapper) {
                    onDropEventFromWrapper(event);
                }
            }
        };
    }

    protected Set<Long> getDraggedTargetList(final DragAndDropEvent event) {
        final com.vaadin.event.dd.TargetDetails targetDet = event.getTargetDetails();
        final Table targetTable = (Table) targetDet.getTarget();
        final Set<Long> targetSelected = getTableValue(targetTable);

        final AbstractSelectTargetDetails dropData = (AbstractSelectTargetDetails) event.getTargetDetails();
        final Long targetItemId = (Long) dropData.getItemIdOver();

        if (entityToBeDeletedIsSelectedInTable(targetItemId, targetSelected)) {
            return targetSelected;
        }
        return Sets.newHashSet(targetItemId);
    }

    private Set<Long> getDraggedTargetList(final TableTransferable transferable, final Table source) {
        @SuppressWarnings("unchecked")
        final AbstractTable<NamedEntity> table = (AbstractTable<NamedEntity>) source;
        return table.getSelectedEntitiesByTransferable(transferable);
    }

    private boolean validateDropList(final Set<?> droplist) {
        if (droplist.isEmpty()) {
            final String actionDidNotWork = i18n.getMessage("message.action.did.not.work");
            notification.displayValidationError(actionDidNotWork);
            return false;
        }
        return true;
    }

    protected boolean isDropValid(final DragAndDropEvent dragEvent) {
        final Transferable transferable = dragEvent.getTransferable();
        final Component compsource = transferable.getSourceComponent();

        final List<String> missingPermissions = hasMissingPermissionsForDrop();
        if (!missingPermissions.isEmpty()) {
            notification.displayValidationError(i18n.getMessage("message.permission.insufficient", missingPermissions));
            return false;
        }

        if (compsource instanceof Table) {
            return validateTable((Table) compsource)
                    && validateDropList(getDraggedTargetList((TableTransferable) transferable, (Table) compsource));
        }

        if (compsource instanceof DragAndDropWrapper) {
            return validateDragAndDropWrapper((DragAndDropWrapper) compsource)
                    && validateDropList(getDraggedTargetList(dragEvent));
        }
        notification.displayValidationError(i18n.getMessage(UIMessageIdProvider.MESSAGE_ACTION_NOT_ALLOWED));
        return false;
    }

    private boolean validateTable(final Table compsource) {
        if (!compsource.getId().equals(getDropTableId())) {
            notification.displayValidationError(UIMessageIdProvider.MESSAGE_ACTION_NOT_ALLOWED);
            return false;
        }
        return true;
    }

    @Override
    public void refreshContainer() {
        final Container container = getContainerDataSource();
        if (!(container instanceof LazyQueryContainer)) {
            return;
        }
        ((LazyQueryContainer) getContainerDataSource()).refresh();
    }

    protected UINotification getNotification() {
        return notification;
    }

    /**
     * Finds the entity object of the given entity ID and performs the
     * publishing of the BaseEntityEventType.SELECTED_ENTITY event
     * 
     * @param entityId
     *            ID of the current entity
     */
    public void selectEntity(final Long entityId) {
        E entity = null;
        if (entityId != null) {
            entity = findEntityByTableValue(entityId).orElse(null);
        }

        setLastSelectedEntityId(entityId);
        publishSelectedEntityEvent(entity);
    }

    protected void selectDroppedEntities(final Long targetId) {
        getSelectedEntities().forEach(this::unselect);
        select(targetId);
    }

    protected void selectDraggedEntities(final AbstractTable<?> source, final Set<Long> ids) {
        source.setValue(ids);
    }

    protected EventBus.UIEventBus getEventBus() {
        return eventBus;
    }

    protected VaadinMessageSource getI18n() {
        return i18n;
    }

    protected SpPermissionChecker getPermChecker() {
        return permChecker;
    }

    protected abstract List<String> hasMissingPermissionsForDrop();

    protected abstract boolean validateDragAndDropWrapper(final DragAndDropWrapper wrapperSource);

    protected abstract void onDropEventFromWrapper(DragAndDropEvent event);

    protected abstract void onDropEventFromTable(DragAndDropEvent event);

    protected abstract String getDropTableId();

    protected abstract AcceptCriterion getDropAcceptCriterion();

    protected abstract void setDataAvailable(boolean available);

}
