/**
 * Licensed to JumpMind Inc under one or more contributor
 * license agreements.  See the NOTICE file distributed
 * with this work for additional information regarding
 * copyright ownership.  JumpMind Inc licenses this file
 * to you under the GNU General Public License, version 3.0 (GPLv3)
 * (the "License"); you may not use this file except in compliance
 * with the License.
 *
 * You should have received a copy of the GNU General Public License,
 * version 3.0 (GPLv3) along with this library; if not, see
 * <http://www.gnu.org/licenses/>.
 *
 * 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.jumpmind.metl.ui.views.design;

import static org.apache.commons.lang.StringUtils.isBlank;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.jumpmind.metl.core.model.ComponentAttribSetting;
import org.jumpmind.metl.core.model.ComponentEntitySetting;
import org.jumpmind.metl.core.model.RelationalModel;
import org.jumpmind.metl.core.model.ModelAttrib;
import org.jumpmind.metl.core.model.ModelEntity;
import org.jumpmind.metl.core.runtime.component.DataDiff;
import org.jumpmind.metl.ui.common.ButtonBar;
import org.jumpmind.metl.ui.common.UiUtils;
import org.jumpmind.vaadin.ui.common.ResizableWindow;

import com.vaadin.data.Container;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.data.util.BeanItemContainer;
import com.vaadin.event.Transferable;
import com.vaadin.event.dd.DragAndDropEvent;
import com.vaadin.event.dd.DropHandler;
import com.vaadin.event.dd.acceptcriteria.AcceptAll;
import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
import com.vaadin.server.FontAwesome;
import com.vaadin.ui.AbstractSelect.AbstractSelectTargetDetails;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.Field;
import com.vaadin.ui.Table;
import com.vaadin.ui.Table.CellStyleGenerator;
import com.vaadin.ui.Table.ColumnGenerator;
import com.vaadin.ui.Table.TableDragMode;
import com.vaadin.ui.TableFieldFactory;
import com.vaadin.ui.TextField;

@SuppressWarnings("serial")
public class EditDataDiffPanel extends AbstractComponentEditPanel {

    private static final long serialVersionUID = 1L;

    Table entityTable = new Table();
    BeanItemContainer<EntitySettings> entitySettingsContainer = new BeanItemContainer<EntitySettings>(EntitySettings.class);
    List<EntitySettings> entitySettings = new ArrayList<EntitySettings>();

    Table attributeTable = new Table();
    BeanItemContainer<AttributeSettings> attributeSettingsContainer = new BeanItemContainer<AttributeSettings>(AttributeSettings.class);
    List<AttributeSettings> attributeSettings = new ArrayList<AttributeSettings>();

    TextField entityFilterField;

    Button editButton;

    EditAttributesWindow attributeWindow;

    Set<EntitySettings> selectedItemIds;

    protected void buildUI() {
        buildButtonBar();
        buildEntityTable();
        fillEntityContainer();
        updateEntityTable(null);
        buildAttributeWindow();

    }

    protected void buildButtonBar() {
        ButtonBar buttonBar = new ButtonBar();
        addComponent(buttonBar);
        if (!readOnly) {
            editButton = buttonBar.addButton("Edit Columns", FontAwesome.EDIT);
            editButton.addClickListener(new EditButtonClickListener());
        }
        entityFilterField = buttonBar.addFilter();
        entityFilterField.addTextChangeListener(event -> updateEntityTable(event.getText()));

        if (!readOnly) {
            Button moveUpButton = buttonBar.addButton("Move Up", FontAwesome.ARROW_UP);
            moveUpButton.addClickListener(new MoveUpClickListener());

            Button moveDownButton = buttonBar.addButton("Move Down", FontAwesome.ARROW_DOWN);
            moveDownButton.addClickListener(new MoveDownClickListener());

            Button moveTopButton = buttonBar.addButton("Move Top", FontAwesome.ANGLE_DOUBLE_UP);
            moveTopButton.addClickListener(new MoveTopClickListener());

            Button moveBottomButton = buttonBar.addButton("Move Bottom", FontAwesome.ANGLE_DOUBLE_DOWN);
            moveBottomButton.addClickListener(new MoveBottomClickListener());

            Button cutButton = buttonBar.addButton("Cut", FontAwesome.CUT);
            cutButton.addClickListener(new CutClickListener());

            Button pasteButton = buttonBar.addButton("Paste", FontAwesome.PASTE);
            pasteButton.addClickListener(new PasteClickListener());
        }

        addComponent(buttonBar);
    }

    @SuppressWarnings("unchecked")
    protected Set<EntitySettings> getSelectedItems() {
        return (Set<EntitySettings>) entityTable.getValue();
    }

    protected EntitySettings getSelectedItem() {
        Set<EntitySettings> selectedItems = getSelectedItems();
        if (selectedItems != null && selectedItems.size() == 1) {
            return selectedItems.iterator().next();
        }
        return null;
    }

    class EditButtonClickListener implements ClickListener {
        private static final long serialVersionUID = 1L;

        public void buttonClick(ClickEvent event) {
            if (getSelectedItem() != null) {
                refreshAttributeContainer((EntitySettings) getSelectedItem());
                updateAttributeTable();
                attributeWindow.show();
            }
        }
    }

    protected void buildEntityTable() {
        entityTable.setContainerDataSource(entitySettingsContainer);
        entityTable.setSelectable(true);
        entityTable.setSortEnabled(false);
        entityTable.setImmediate(true);
        entityTable.setSizeFull();
        entityTable.addGeneratedColumn("entityName", new ColumnGenerator() {
            private static final long serialVersionUID = 1L;

            @Override
            public Object generateCell(Table source, Object itemId, Object columnId) {
                EntitySettings setting = (EntitySettings) itemId;
                RelationalModel model = (RelationalModel) component.getInputModel();
                ModelEntity entity = model.getEntityById(setting.getEntityId());
                return UiUtils.getName(entityFilterField.getValue(), entity.getName());
            }
        });
        entityTable.setVisibleColumns(new Object[] { "entityName", "addEnabled", "updateEnabled", "deleteEnabled" });
        entityTable.setColumnWidth("entityName", 250);
        entityTable.setColumnHeaders(new String[] { "Entity Name", "Add Enabled", "Chg Enabled", "Del Enabled" });
        entityTable.setColumnExpandRatio("entityName", 1);
        entityTable.setTableFieldFactory(new EditEntityFieldFactory());
        entityTable.setEditable(true);
        entityTable.setMultiSelect(true);
        if (!readOnly) {
            entityTable.setDragMode(TableDragMode.MULTIROW);
            entityTable.setDropHandler(new TableDropHandler());
        }
        entityTable.setCellStyleGenerator(new TableCellStyleGenerator());
        addComponent(entityTable);
        setExpandRatio(entityTable, 1.0f);
    }

    class TableCellStyleGenerator implements CellStyleGenerator {
        public String getStyle(Table source, Object itemId, Object propertyId) {
            if (propertyId != null && selectedItemIds != null && selectedItemIds.contains(itemId)) {
                return "highlight";
            }
            return null;
        }
    }

    protected void fillEntityContainer() {
        if (component.getInputModel() != null) {

            for (ModelEntity entity : ((RelationalModel)component.getInputModel()).getModelEntities()) {
                ComponentEntitySetting insert = component.getSingleEntitySetting(entity.getId(), DataDiff.ENTITY_ADD_ENABLED);
                ComponentEntitySetting update = component.getSingleEntitySetting(entity.getId(), DataDiff.ENTITY_CHG_ENABLED);
                ComponentEntitySetting delete = component.getSingleEntitySetting(entity.getId(), DataDiff.ENTITY_DEL_ENABLED);
                ComponentEntitySetting ordinal = component.getSingleEntitySetting(entity.getId(), DataDiff.ENTITY_ORDER);
                boolean insertEnabled = insert != null ? Boolean.parseBoolean(insert.getValue()) : true;
                boolean updateEnabled = update != null ? Boolean.parseBoolean(update.getValue()) : true;
                boolean deleteEnabled = delete != null ? Boolean.parseBoolean(delete.getValue()) : true;
                Integer order = ordinal != null ? Integer.parseInt(ordinal.getValue()) : 0;
                entitySettings.add(new EntitySettings(entity.getId(), insertEnabled, updateEnabled, deleteEnabled, order));
            }

            Collections.sort(entitySettings, new Comparator<EntitySettings>() {
                public int compare(EntitySettings o1, EntitySettings o2) {
                    return new Integer(o1.getOrdinalSetting()).compareTo(new Integer(o2.getOrdinalSetting()));
                }
            });
        }
    }

    protected void updateEntityTable(String filter) {
        filter = filter != null ? filter.toLowerCase() : null;
        entityFilterField.setValue(filter);
        entityTable.removeAllItems();
        for (EntitySettings entitySetting : entitySettings) {
            RelationalModel model = (RelationalModel) component.getInputModel();
            ModelEntity entity = model.getEntityById(entitySetting.getEntityId());
            if (isBlank(filter) || entity.getName().toLowerCase().contains(filter)) {
                entityTable.addItem(entitySetting);
            }
        }
    }

    protected void moveItemsTo(Set<EntitySettings> itemIds, int index) {
        if (index >= 0 && index < entitySettingsContainer.getItemIds().size() && itemIds.size() > 0) {
            int firstItemIndex = entitySettingsContainer.indexOfId(itemIds.iterator().next());
            if (index != firstItemIndex) {
                for (EntitySettings itemId : itemIds) {
                    boolean movingUp = index < entitySettingsContainer.indexOfId(itemId);
                    entitySettingsContainer.removeItem(itemId);
                    entitySettingsContainer.addItemAt(index, itemId);
                    if (movingUp) {
                        index++;
                    }
                }
                calculatePositions();
                saveOrdinalSettings();
            }
        }
    }

    protected void saveOrdinalSettings() {
        String attrName = DataDiff.ENTITY_ORDER;
        int ordinal = 1;
        for (EntitySettings record : entitySettingsContainer.getItemIds()) {
            saveSetting(record.getEntityId(), attrName, String.valueOf(ordinal));
            ordinal++;
        }
    }

    protected void saveSetting(String entityId, String name, String value) {
        ComponentEntitySetting setting = component.getSingleEntitySetting(entityId, name);
        if (setting == null) {
            setting = new ComponentEntitySetting(entityId, name, value);
            setting.setComponentId(component.getId());
            component.addEntitySetting(setting);
            context.getConfigurationService().save(setting);
        } else if (!StringUtils.equals(setting.getValue(), value)) {
            setting.setValue(value);
            context.getConfigurationService().save(setting);
        }
    }

    protected void calculatePositions() {
        boolean needsRefreshed = false;

        int ordinal = 1;
        for (EntitySettings record : entitySettingsContainer.getItemIds()) {
            if (record.getOrdinalSetting() != ordinal) {
                record.setOrdinalSetting(ordinal);
                needsRefreshed = true;
            }
            ordinal++;
        }
        if (needsRefreshed) {
            entityTable.refreshRowCache();
        }
    }

    class EditEntityFieldFactory implements TableFieldFactory {
        private static final long serialVersionUID = 1L;

        public Field<?> createField(final Container dataContainer, final Object itemId, final Object propertyId,
                com.vaadin.ui.Component uiContext) {
            final EntitySettings settings = (EntitySettings) itemId;

            if (propertyId.equals("addEnabled")) {
                return createEntityCheckBox(settings, DataDiff.ENTITY_ADD_ENABLED);
            } else if (propertyId.equals("updateEnabled")) {
                return createEntityCheckBox(settings, DataDiff.ENTITY_CHG_ENABLED);
            } else if (propertyId.equals("deleteEnabled")) {
                return createEntityCheckBox(settings, DataDiff.ENTITY_DEL_ENABLED);
            } else {
                return null;
            }
        }
    }

    protected CheckBox createEntityCheckBox(final EntitySettings settings, final String key) {
        final CheckBox checkBox = new CheckBox();
        checkBox.setImmediate(true);
        checkBox.addValueChangeListener(new ValueChangeListener() {
            private static final long serialVersionUID = 1L;

            @Override
            public void valueChange(ValueChangeEvent event) {
                ComponentEntitySetting setting = component.getSingleEntitySetting(settings.getEntityId(), key);

                String oldValue = setting == null ? Boolean.TRUE.toString() : setting.getValue();
                if (setting == null) {
                    setting = new ComponentEntitySetting(settings.getEntityId(), component.getId(), key, Boolean.TRUE.toString());
                    component.addEntitySetting(setting);
                }
                setting.setValue(checkBox.getValue().toString());
                if (!oldValue.equals(setting.getValue())) {
                    context.getConfigurationService().save(setting);
                }
            }
        });
        checkBox.setReadOnly(readOnly);
        return checkBox;
    }

    public static class EntitySettings implements Serializable {
        private static final long serialVersionUID = 1L;
        String entityId;
        boolean addEnabled;
        boolean updateEnabled;
        boolean deleteEnabled;
        int ordinalSetting;

        public EntitySettings(String entityId, boolean insertEnabled, boolean updateEnabled, boolean deleteEnabled, int ordinal) {
            this.entityId = entityId;
            this.addEnabled = insertEnabled;
            this.updateEnabled = updateEnabled;
            this.deleteEnabled = deleteEnabled;
            this.ordinalSetting = ordinal;
        }

        public void setAddEnabled(boolean insertEnabled) {
            this.addEnabled = insertEnabled;
        }

        public boolean isAddEnabled() {
            return addEnabled;
        }

        public void setUpdateEnabled(boolean updateEnabled) {
            this.updateEnabled = updateEnabled;
        }

        public boolean isUpdateEnabled() {
            return updateEnabled;
        }

        public void setDeleteEnabled(boolean deleteEnabled) {
            this.deleteEnabled = deleteEnabled;
        }

        public boolean isDeleteEnabled() {
            return deleteEnabled;
        }

        public String getEntityId() {
            return entityId;
        }

        public void setEntityId(String entityId) {
            this.entityId = entityId;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof EntitySettings) {
                return entityId.equals(((EntitySettings) obj).getEntityId());
            } else {
                return super.equals(obj);
            }
        }

        @Override
        public int hashCode() {
            return entityId.hashCode();
        }

        public int getOrdinalSetting() {
            return ordinalSetting;
        }

        public void setOrdinalSetting(int ordinalSetting) {
            this.ordinalSetting = ordinalSetting;
        }
    }

    // attribute window and support

    protected void buildAttributeWindow() {
        attributeWindow = new EditAttributesWindow();
    }

    class EditAttributesWindow extends ResizableWindow {
        private static final long serialVersionUID = 1L;

        public EditAttributesWindow() {
            super("Edit Columns to Compare");
            setWidth(800f, Unit.PIXELS);
            setHeight(600f, Unit.PIXELS);
            content.setMargin(true);
            buildAttributeTable();
            addComponent(buildButtonFooter(buildCloseButton()));
        }

        private void buildAttributeTable() {

            attributeTable.setContainerDataSource(attributeSettingsContainer);
            attributeTable.setSelectable(true);
            attributeTable.setSortEnabled(false);
            attributeTable.setImmediate(true);
            attributeTable.setSortEnabled(false);
            attributeTable.setSizeFull();
            attributeTable.addGeneratedColumn("attributeName", new ColumnGenerator() {
                private static final long serialVersionUID = 1L;

                @Override
                public Object generateCell(Table source, Object itemId, Object columnId) {
                    AttributeSettings setting = (AttributeSettings) itemId;
                    RelationalModel model = (RelationalModel) component.getInputModel();
                    ModelAttrib attribute = model.getAttributeById(setting.getAttributeId());
                    return UiUtils.getName(entityFilterField.getValue(), attribute.getName());
                }
            });
            attributeTable.setVisibleColumns(new Object[] { "attributeName", "compareEnabled" });
            attributeTable.setColumnWidth("attributeName", 250);
            attributeTable.setColumnHeaders(new String[] { "Attribute Name", "Compare Enabled" });
            attributeTable.setColumnExpandRatio("attributeName", 1);
            attributeTable.setTableFieldFactory(new EditAttributeFieldFactory());
            attributeTable.setEditable(true);
            addComponent(attributeTable, 1);

        }
    }

    private void refreshAttributeContainer(EntitySettings selectedRow) {
        attributeSettings.clear();
        ModelEntity entity = ((RelationalModel)component.getInputModel()).getEntityById(selectedRow.getEntityId());
        for (ModelAttrib attribute : entity.getModelAttributes()) {

            ComponentAttribSetting compare = component.getSingleAttributeSetting(attribute.getId(), DataDiff.ATTRIBUTE_COMPARE_ENABLED);
            boolean compareEnabled = compare != null ? Boolean.parseBoolean(compare.getValue()) : true;
            attributeSettings.add(new AttributeSettings(attribute.getId(), compareEnabled, attribute.isPk() == true ? true : false));
        }
    }

    protected void updateAttributeTable() {
        attributeTable.removeAllItems();
        for (AttributeSettings attributeSetting : attributeSettings) {
            attributeTable.addItem(attributeSetting);
        }
    }

    class EditAttributeFieldFactory implements TableFieldFactory {
        private static final long serialVersionUID = 1L;

        public Field<?> createField(final Container dataContainer, final Object itemId, final Object propertyId,
                com.vaadin.ui.Component uiContext) {
            final AttributeSettings settings = (AttributeSettings) itemId;
            if (propertyId.equals("compareEnabled")) {
                return createAttributeCheckBox(settings, DataDiff.ATTRIBUTE_COMPARE_ENABLED);
            } else {
                return null;
            }
        }
    }

    protected CheckBox createAttributeCheckBox(final AttributeSettings settings, final String key) {
        final CheckBox checkBox = new CheckBox();
        checkBox.setImmediate(true);
        if (settings.isPrimaryKey()) {
            checkBox.setEnabled(false);
        }
        checkBox.addValueChangeListener(new ValueChangeListener() {
            private static final long serialVersionUID = 1L;

            @Override
            public void valueChange(ValueChangeEvent event) {
                ComponentAttribSetting setting = component.getSingleAttributeSetting(settings.getAttributeId(), key);

                String oldValue = setting == null ? Boolean.TRUE.toString() : setting.getValue();
                if (setting == null) {
                    setting = new ComponentAttribSetting(settings.getAttributeId(), component.getId(), key, Boolean.TRUE.toString());
                    component.addAttributeSetting(setting);
                }
                setting.setValue(checkBox.getValue().toString());
                if (!oldValue.equals(setting.getValue())) {
                    context.getConfigurationService().save(setting);
                }
            }
        });
        checkBox.setReadOnly(readOnly);
        return checkBox;
    }

    public static class AttributeSettings implements Serializable {
        private static final long serialVersionUID = 1L;
        boolean primaryKey;
        String attributeId;
        boolean compareEnabled;

        public AttributeSettings(String attributeId, boolean compareEnabled, boolean primaryKey) {
            this.attributeId = attributeId;
            this.compareEnabled = compareEnabled;
            this.primaryKey = primaryKey;
        }

        public void setCompareEnabled(boolean compareEnabled) {
            this.compareEnabled = compareEnabled;
        }

        public boolean isCompareEnabled() {
            return compareEnabled;
        }

        public String getAttributeId() {
            return attributeId;
        }

        public void setAttributeId(String attributeId) {
            this.attributeId = attributeId;
        }

        public void setPrimaryKey(boolean primaryKey) {
            this.primaryKey = primaryKey;
        }

        public boolean isPrimaryKey() {
            return primaryKey;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof AttributeSettings) {
                return attributeId.equals(((AttributeSettings) obj).getAttributeId());
            } else {
                return super.equals(obj);
            }
        }
    }

    class MoveUpClickListener implements ClickListener {
        public void buttonClick(ClickEvent event) {
            Set<EntitySettings> itemIds = getSelectedItems();
            if (itemIds.size() > 0 && itemIds != null) {
                EntitySettings firstItem = itemIds.iterator().next();
                int index = entitySettingsContainer.indexOfId(firstItem) - 1;
                moveItemsTo(getSelectedItems(), index);
            }
        }
    }

    class MoveDownClickListener implements ClickListener {
        public void buttonClick(ClickEvent event) {
            Set<EntitySettings> itemIds = getSelectedItems();
            if (itemIds.size() > 0 && itemIds != null) {
                EntitySettings lastItem = null;
                Iterator<EntitySettings> iter = itemIds.iterator();
                while (iter.hasNext()) {
                    lastItem = iter.next();
                }
                int index = entitySettingsContainer.indexOfId(lastItem) + 1;
                moveItemsTo(getSelectedItems(), index);
            }
        }
    }

    class MoveTopClickListener implements ClickListener {
        public void buttonClick(ClickEvent event) {
            moveItemsTo(getSelectedItems(), 0);
        }
    }

    class MoveBottomClickListener implements ClickListener {
        public void buttonClick(ClickEvent event) {
            moveItemsTo(getSelectedItems(), entitySettingsContainer.size() - 1);
        }
    }

    class CutClickListener implements ClickListener {
        public void buttonClick(ClickEvent event) {
            Set<EntitySettings> itemIds = getSelectedItems();
            selectedItemIds = new LinkedHashSet<EntitySettings>(itemIds);
            for (EntitySettings itemId : itemIds) {
                entityTable.unselect(itemId);
            }
            entityTable.refreshRowCache();
        }
    }

    class PasteClickListener implements ClickListener {
        public void buttonClick(ClickEvent event) {
            Set<EntitySettings> itemIds = getSelectedItems();
            if (itemIds.size() > 0 && selectedItemIds != null) {
                int index = entitySettingsContainer.indexOfId(itemIds.iterator().next());
                moveItemsTo(selectedItemIds, index);
                selectedItemIds = null;
            }
        }
    }

    class TableDropHandler implements DropHandler {
        public void drop(DragAndDropEvent event) {
            AbstractSelectTargetDetails targetDetails = (AbstractSelectTargetDetails) event.getTargetDetails();
            Transferable transferable = event.getTransferable();
            if (transferable.getSourceComponent() == entityTable) {
                EntitySettings target = (EntitySettings) targetDetails.getItemIdOver();
                moveItemsTo(getSelectedItems(), entitySettingsContainer.indexOfId(target));
            }
        }

        public AcceptCriterion getAcceptCriterion() {
            return AcceptAll.get();
        }
    }
}
