/*******************************************************************************
 * Copyright 2017-2025 TAXTELECOM, LLC
 *
 * 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 ru.taximaxim.codekeeper.ui.differ;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.fieldassist.ContentProposalAdapter;
import org.eclipse.jface.fieldassist.SimpleContentProposalProvider;
import org.eclipse.jface.fieldassist.TextContentAdapter;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ICheckStateProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeViewerListener;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.TreeExpansionEvent;
import org.eclipse.jface.viewers.TreeViewerColumn;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.ISharedImages;
import org.pgcodekeeper.core.DatabaseType;
import org.pgcodekeeper.core.ignorelist.IgnoreList;
import org.pgcodekeeper.core.library.PgLibrarySource;
import org.pgcodekeeper.core.model.difftree.DbObjType;
import org.pgcodekeeper.core.model.difftree.DiffTree;
import org.pgcodekeeper.core.model.difftree.TreeElement;
import org.pgcodekeeper.core.model.difftree.TreeElement.DiffSide;
import org.pgcodekeeper.core.model.difftree.TreeFlattener;
import org.pgcodekeeper.core.model.exporter.ModelExporter;
import org.pgcodekeeper.core.model.graph.DepcyFinder;
import org.pgcodekeeper.core.schema.AbstractDatabase;
import org.pgcodekeeper.core.schema.PgStatement;
import org.pgcodekeeper.core.settings.ISettings;
import org.pgcodekeeper.core.utils.FileUtils;

import ru.taximaxim.codekeeper.ui.Activator;
import ru.taximaxim.codekeeper.ui.AggregatingListener;
import ru.taximaxim.codekeeper.ui.Log;
import ru.taximaxim.codekeeper.ui.ProjectIcon;
import ru.taximaxim.codekeeper.ui.UIConsts;
import ru.taximaxim.codekeeper.ui.UIConsts.PG_EDIT_PREF;
import ru.taximaxim.codekeeper.ui.UIConsts.PLUGIN_ID;
import ru.taximaxim.codekeeper.ui.UiSync;
import ru.taximaxim.codekeeper.ui.comparetools.CompareAction;
import ru.taximaxim.codekeeper.ui.comparetools.CompareInput;
import ru.taximaxim.codekeeper.ui.dialogs.BuildDepsGraphDialog;
import ru.taximaxim.codekeeper.ui.dialogs.ExceptionNotifier;
import ru.taximaxim.codekeeper.ui.dialogs.FilterDialog;
import ru.taximaxim.codekeeper.ui.differ.filters.AbstractFilter;
import ru.taximaxim.codekeeper.ui.differ.filters.CodeFilter;
import ru.taximaxim.codekeeper.ui.differ.filters.ContainerFilter;
import ru.taximaxim.codekeeper.ui.differ.filters.UserFilter;
import ru.taximaxim.codekeeper.ui.localizations.Messages;
import ru.taximaxim.codekeeper.ui.utils.FileUtilsUi;
import ru.taximaxim.codekeeper.ui.utils.UiUtils;
import ru.taximaxim.codekeeper.ui.xmlstore.ListXmlStore;

/**
 * Use {@link #setChecked(TreeElement, boolean)} for any kind of checkbox work.
 * It refreshes all the needed states without calling slow full viewer refresh.
 * Always call {@link #viewerChecksUpdated()} after finishing checkbox work.
 */
public class DiffTableViewer extends Composite {

    private static final String EMPTY_STRING = ""; //$NON-NLS-1$
    private static final Pattern REGEX_SPECIAL_CHARS = Pattern.compile("[\\[\\\\\\^$.|?*+()]"); //$NON-NLS-1$
    private static final String GITLABEL_PROP = "GITLABEL_PROP"; //$NON-NLS-1$

    private static final ListXmlStore XML_HISTORY = new ListXmlStore(200, "fhistory.xml", "history", "element"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    private static final Object[] EMPTY_ARRAY = new Object[0];

    private final boolean showGitUser;
    private boolean showDbUser;
    private boolean showLibrary;

    private final Image iSideBoth;
    private final Image iSideLeft;
    private final Image iSideRight;

    private final boolean viewOnly;
    private final Path location;
    private final Map<TreeElement, ElementMetaInfo> elementInfoMap = new HashMap<>();
    private final Set<TreeElement> elements = elementInfoMap.keySet();
    private final DiffContentProvider contentProvider = new DiffContentProvider();
    private final CheckStateProvider checkProvider;
    private final TableViewerComparator comparator = new TableViewerComparator();
    private Set<TreeElement> tables;
    private IStructuredSelection oldSelection;
    private IStructuredSelection newSelection;

    private final Text txtFilterName;
    private final Button useRegEx;
    private Label lblObjectCount;
    private Label lblCheckedCount;

    private final CheckboxTreeViewer viewer;
    private final TableViewerFilter viewerFilter = new TableViewerFilter();
    private TreeViewerColumn columnCheck;
    private TreeViewerColumn columnType;
    private TreeViewerColumn columnChange;
    private TreeViewerColumn columnName;
    private TreeViewerColumn columnGitUser;
    private TreeViewerColumn columnDbUser;
    private TreeViewerColumn columnLocation;
    private TreeViewerColumn columnLibrary;

    private DbSource dbProject;
    private DbSource dbRemote;
    private DatabaseType dbType;
    private final ISettings settings;

    private boolean isApplyToProj = true;

    private final IStatusLineManager lineManager;

    private final List<ICheckStateListener> programmaticCheckListeners = new ArrayList<>();

    private enum Columns {
        CHECK, LIBRARY, NAME, TYPE, CHANGE, LOCATION, GIT_USER, DB_USER,
    }

    public StructuredViewer getViewer() {
        return viewer;
    }

    public Collection<TreeElement> getElements() {
        return Collections.unmodifiableCollection(elements);
    }

    public DiffTableViewer(Composite parent, boolean viewOnly, DatabaseType databaseType, ISettings settings) {
        this(parent, viewOnly, null, null, databaseType, settings);
    }

    public DiffTableViewer(Composite parent, boolean viewOnly, IStatusLineManager lineManager, Path location,
            DatabaseType databaseType, ISettings settings) {
        super(parent, SWT.NONE);
        this.viewOnly = viewOnly;
        this.lineManager = lineManager;
        this.location = location;
        this.dbType = databaseType;
        this.settings = settings;
        showGitUser = location != null
                && Activator.getDefault().getPreferenceStore().getBoolean(PG_EDIT_PREF.SHOW_GIT_USER)
                && GitUserReader.checkRepo(location);

        PixelConverter pc = new PixelConverter(this);

        iSideBoth = Activator.getRegisteredImage(ProjectIcon.EDIT);
        iSideRight = Activator.getEclipseImage(ISharedImages.IMG_OBJ_ADD);
        iSideLeft = Activator.getEclipseImage(ISharedImages.IMG_ETOOL_DELETE);

        GridLayout gl = new GridLayout();
        gl.marginHeight = gl.marginWidth = 0;
        setLayout(gl);

        // upper composite
        Composite upperComp = new Composite(this, SWT.NONE);
        upperComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        gl = new GridLayout(viewOnly ? 3 : 4, false);
        gl.marginWidth = gl.marginHeight = 0;
        upperComp.setLayout(gl);

        if (!viewOnly) {
            ToolBarManager mgrTblBtn = new ToolBarManager(SWT.FLAT | SWT.RIGHT);

            mgrTblBtn.add(new Action(Messages.select_all, Activator.getRegisteredDescriptor(ProjectIcon.SELECT_ALL)) {

                @Override
                public void run() {
                    setElementsChecked(elements, true, true);
                }
            });

            mgrTblBtn.add(new Action(Messages.select_none, Activator.getRegisteredDescriptor(ProjectIcon.SELECT_NONE)) {

                @Override
                public void run() {
                    setElementsChecked(elements, false, true);
                }
            });

            mgrTblBtn.add(new Action(Messages.diffTableViewer_invert_selection,
                    Activator.getRegisteredDescriptor(ProjectIcon.INVERT_SELECTION)) {

                @Override
                public void run() {
                    setElementsChecked(elements, el -> !el.isSelected(), true);
                }
            });

            mgrTblBtn.add(new Action(Messages.DiffTableViewer_copy_as_regex, ImageDescriptor
                    .createFromImage(Activator.getEclipseImage(ISharedImages.IMG_TOOL_COPY))) {

                @Override
                public void run() {
                    saveCheckedElements2ClipboardAsExpession();
                }
            });

            mgrTblBtn.add(new Action(Messages.DiffTableViewer_show_filters,
                    Activator.getRegisteredDescriptor(ProjectIcon.EMPTY_FILTER)) {

                @Override
                public void run() {
                    FilterDialog dialog = new FilterDialog(getShell(),
                            viewerFilter.containerFilter, viewerFilter.codeFilter,
                            viewerFilter.gitUserFilter, viewerFilter.dbUserFilter,
                            viewerFilter.types, viewerFilter.sides,
                            viewerFilter.isLocalChange, viewerFilter.isHideLibs,
                            isApplyToProj, dbType);
                    if (dialog.open() == Window.OK) {
                        setImageDescriptor(Activator.getRegisteredDescriptor(
                                viewerFilter.isAdvancedEmpty() ? ProjectIcon.EMPTY_FILTER : ProjectIcon.FILTER));
                        viewer.refresh();
                    }
                }
            });

            mgrTblBtn.createControl(upperComp);
        }

        txtFilterName = new Text(upperComp, SWT.BORDER | SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL);
        GridData gd = new GridData(SWT.FILL, SWT.CENTER, false, false);
        gd.widthHint = pc.convertWidthInCharsToPixels(30);
        txtFilterName.setLayoutData(gd);
        txtFilterName.setMessage(Messages.DiffTableViewer_filter_placeholder);

        List<String> history = new ArrayList<>();
        try {
            history = XML_HISTORY.readObjects();
        } catch (IOException ex) {
            Log.log(ex);
        }

        SimpleContentProposalProvider scp = new SimpleContentProposalProvider(history.toArray(new String[history.size()]));
        scp.setFiltering(true);

        ContentProposalAdapter adapter = new ContentProposalAdapter(txtFilterName,
                new TextContentAdapter(), scp, null, null);
        adapter.setPopupSize(new Point(pc.convertWidthInCharsToPixels(40), pc.convertHeightInCharsToPixels(8)));
        adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);

        useRegEx = new Button(upperComp, SWT.CHECK);
        useRegEx.setToolTipText(Messages.diffTableViewer_use_java_regular_expressions_see_more);
        useRegEx.setText(Messages.diffTableViewer_use_regular_expressions);
        useRegEx.setLayoutData(new GridData(SWT.DEFAULT, SWT.CENTER, false, false));
        useRegEx.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                viewerFilter.setUseRegEx(useRegEx.getSelection());
                viewer.refresh();
            }
        });

        Composite container = new Composite(upperComp, SWT.NONE);
        container.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
        if (lineManager != null) {
            createRightSide(container);
        } else {
            container.setLayout(new GridLayout(viewOnly ? 2 : 3, false));
            Label l = new Label(container, SWT.NONE);
            l.setEnabled(false);
            l.setText("|"); //$NON-NLS-1$

            if (!viewOnly) {
                lblCheckedCount = new Label(container, SWT.NONE);
            }
            lblObjectCount = new Label(container, SWT.NONE);
        }

        updateObjectsLabels();
        // end upper composite

        int viewerStyle = SWT.MULTI | SWT.FULL_SELECTION | SWT.BORDER;
        if (!viewOnly) {
            viewerStyle |= SWT.CHECK;
        }
        viewer = new CheckboxTreeViewer(new Tree(this, viewerStyle));

        ModifyListener listener = new ModifyListener() {

            @Override
            public void modifyText(ModifyEvent e) {
                String text = ((Text) e.getSource()).getText().strip();
                filterHistory(text);
                viewerFilter.setFilter(text);
                viewer.refresh();
            }

            private void filterHistory(String text) {
                try {
                    if (text != null && !text.isEmpty()) {
                        XML_HISTORY.addHistoryEntry(text);
                    }
                    List<String> history = XML_HISTORY.readObjects();
                    scp.setProposals(history.toArray(new String[history.size()]));
                } catch (IOException e) {
                    Log.log(e);
                }
            }
        };

        AggregatingListener.addModifyListener(txtFilterName, listener);

        viewer.addSelectionChangedListener(event -> {
            oldSelection = newSelection;
            newSelection = (IStructuredSelection)event.getSelection();
        });

        viewer.getControl().addKeyListener(new KeyAdapter() {

            @Override
            public void keyPressed(KeyEvent e) {
                if (UiUtils.isCommandCopy(e)) {
                    copyObjectNamesToClipboard();
                    e.doit = false;
                }
            }
        });

        viewer.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        viewer.getTree().setLinesVisible(true);
        viewer.getTree().setHeaderVisible(true);

        viewer.getControl().setMenu(
                getViewerMenu().createContextMenu(viewer.getControl()));

        if (!viewOnly) {
            viewer.addCheckStateListener(new CheckStateListener());
            checkProvider = new CheckStateProvider();
            viewer.setCheckStateProvider(checkProvider);
            viewer.addTreeListener(new RefreshCheckedTreeListener());
        } else {
            checkProvider = null;
        }
        // resolves a bottleneck in findItems() which is very hot
        // and slow when using native API impl
        viewer.setUseHashlookup(true);
        viewer.setComparator(comparator);
        viewer.setFilters(viewerFilter);
        initColumns();
        viewer.setContentProvider(contentProvider);
    }

    public void createRightSide(Composite parent) {
        // will be overridden by subclasses if needed
    }

    private MenuManager getViewerMenu() {
        MenuManager menuMgr = new MenuManager();
        if (!viewOnly) {
            menuMgr.add(new Action(Messages.diffTableViewer_select_child_elements) {

                @Override
                public void run() {
                    setSelectionSubtreesChecked((IStructuredSelection) viewer.getSelection(), true);
                }
            });
            menuMgr.add(new Action(Messages.diffTableViewer_deselect_child_elements) {

                @Override
                public void run() {
                    setSelectionSubtreesChecked((IStructuredSelection) viewer.getSelection(), false);
                }
            });
            menuMgr.add(new Separator());
            menuMgr.add(new Action(Messages.diffTableViewer_mark_selected_elements) {

                @Override
                public void run() {
                    setElementsChecked(((IStructuredSelection) viewer.getSelection()).toList(), true, false);
                }
            });
            menuMgr.add(new Action(Messages.diffTableViewer_unmark_selected_elements) {

                @Override
                public void run() {
                    setElementsChecked(((IStructuredSelection) viewer.getSelection()).toList(), false, false);
                }
            });
            menuMgr.add(new Separator());
        }
        menuMgr.add(new Action(Messages.DiffTableViewer_expand_all) {

            @Override
            public void run() {
                viewer.expandAll();
            }

            @Override
            public boolean isEnabled() {
                return true;
            }
        });
        menuMgr.add(new Action(Messages.DiffTableViewer_collapse_all) {

            @Override
            public void run() {
                viewer.collapseAll();
            }

            @Override
            public boolean isEnabled() {
                return true;
            }
        });
        menuMgr.add(new Separator());
        menuMgr.add(new Action(Messages.DiffTableViewer_copy_object_names + "\tCtrl+C") { //$NON-NLS-1$

            @Override
            public void run() {
                copyObjectNamesToClipboard();
            }
        });
        menuMgr.add(new Separator());
        menuMgr.add(new Action(Messages.diffTableViewer_open_diff_in_new_window) {

            @Override
            public void run() {
                TreeElement el = (TreeElement)
                        ((IStructuredSelection) viewer.getSelection()).getFirstElement();
                PgStatement remote = el.getSide() == DiffSide.LEFT ? null
                        : el.getPgStatement(dbRemote.getDbObject());
                PgStatement project = el.getSide() == DiffSide.RIGHT ? null
                        : el.getPgStatement(dbProject.getDbObject());
                CompareAction.openCompareEditor(new CompareInput(el.getName(),
                        el.getType(), remote, project, settings));
            }
        });

        menuMgr.add(new Separator());
        menuMgr.add(new Action(Messages.DiffTableViewer_menu_build_graph) {

            @Override
            public void run() {
                writeDeps((TreeElement) ((IStructuredSelection) viewer.getSelection()).getFirstElement());
            }
        });

        menuMgr.addMenuListener(manager -> {
            boolean enable = !viewer.getSelection().isEmpty();
            for (IContributionItem item : manager.getItems()) {
                if (item instanceof ActionContributionItem actionItem) {
                    actionItem.getAction().setEnabled(enable);
                }
            }
        });

        return menuMgr;
    }

    protected void writeDeps(TreeElement el) {
        String objName = el.getQualifiedName();
        boolean isBothEnabled = el.getSide() == DiffSide.BOTH;
        boolean isProject = el.getSide() == DiffSide.LEFT;

        BuildDepsGraphDialog graphDlg = new BuildDepsGraphDialog(getShell(), objName, isProject, isBothEnabled, dbType);
        if (graphDlg.open() != Window.OK) {
            return;
        }
        AbstractDatabase source = graphDlg.isProject() ? dbProject.getDbObject() : dbRemote.getDbObject();
        PgStatement st = el.getPgStatement(source);
        List<String> deps = DepcyFinder.byStatement(graphDlg.getGraphDepth(), graphDlg.isReverse(),
                graphDlg.getObjTypes(), st);

        String content = String.join("\n", deps); //$NON-NLS-1$
        try {
            FileUtilsUi.saveOpenTmpSqlEditor(content, "dependencies_for_" + objName, settings.getDbType()); //$NON-NLS-1$
        } catch (IOException | CoreException ex) {
            ExceptionNotifier.notifyDefault(Messages.DiffTableViewer_error_creating_graph, ex);
        }
    }

    private void copyObjectNamesToClipboard() {
        IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
        if (selection.isEmpty()) {
            return;
        }

        StringBuilder sb = new StringBuilder();
        for (Object r : selection.toList()) {
            TreeElement element = (TreeElement) r;
            sb.append(element.getQualifiedName());
            sb.append(UIConsts._NL);
        }

        Clipboard cb = new Clipboard(viewer.getControl().getDisplay());
        cb.setContents(new Object[] { sb.toString() },
                new Transfer[] { TextTransfer.getInstance() });
    }

    private void initColumns() {
        ColumnViewerToolTipSupport.enableFor(viewer);

        columnCheck = createColumn(Columns.CHECK);
        columnCheck.setLabelProvider(new ColumnLabelProvider() {
            @Override
            public String getText(Object element) {
                return EMPTY_STRING;
            }
        });

        columnLibrary = createColumn(Columns.LIBRARY);
        columnLibrary.getColumn().setImage(Activator.getRegisteredImage(ProjectIcon.LIB));
        columnLibrary.setLabelProvider(new ColumnLabelProvider() {

            @Override
            public String getText(Object element) {
                return EMPTY_STRING;
            }

            @Override
            public Image getImage(Object element) {
                return isLibElement(element) ? Activator.getRegisteredImage(ProjectIcon.LIB) : null;
            }

            @Override
            public String getToolTipText(Object element) {
                ElementMetaInfo meta = elementInfoMap.get(element);
                if (meta != null) {
                    String libLocation = meta.getLibLocation();
                    if (libLocation != null) {
                        return libLocation;
                    }
                }

                return null;
            }
        });

        columnName = createColumn(Columns.NAME);
        columnName.setLabelProvider(new StyledCellLabelProvider(){

            @Override
            public void update(ViewerCell cell) {
                String name = ((TreeElement)cell.getElement()).getName();
                cell.setText(name);

                Region loc = viewerFilter.getMatchingLocation(name, viewerFilter.filterName,
                        viewerFilter.useRegEx ? viewerFilter.regExPattern : null);
                if (loc != null) {
                    StyleRange highlightMatch = new StyleRange(loc.getOffset(),
                            loc.getLength(), null,
                            getDisplay().getSystemColor(SWT.COLOR_YELLOW));
                    cell.setStyleRanges(new StyleRange[] { highlightMatch });
                } else {
                    cell.setStyleRanges(null);
                }
                super.update(cell);
            }
        });

        columnType = createColumn(Columns.TYPE);
        columnType.setLabelProvider(new ColumnLabelProvider() {

            @Override
            public String getText(Object element) {
                return getElementType((TreeElement) element).getTypeName();
            }

            @Override
            public Image getImage(Object element) {
                return Activator.getDbObjImage(getElementType((TreeElement) element));
            }

            private DbObjType getElementType(TreeElement element) {
                return element.getType();
            }
        });

        columnChange = createColumn(Columns.CHANGE);
        columnChange.setLabelProvider(new ColumnLabelProvider() {

            @Override
            public String getText(Object element) {
                return switch (((TreeElement) element).getSide()) {
                    case BOTH -> isApplyToProj ? "edit" : "ALTER"; //$NON-NLS-1$ //$NON-NLS-2$
                    case LEFT -> isApplyToProj ? "delete" : "CREATE"; //$NON-NLS-1$ //$NON-NLS-2$
                    case RIGHT -> isApplyToProj ? "add" : "DROP"; //$NON-NLS-1$ //$NON-NLS-2$
                    default -> null;
                };
            }

            @Override
            public Image getImage(Object element) {
                return switch (((TreeElement) element).getSide()) {
                    case BOTH -> iSideBoth;
                    case LEFT -> isApplyToProj ? iSideLeft : iSideRight;
                    case RIGHT -> isApplyToProj ? iSideRight : iSideLeft;
                    default -> null;
                };
            }
        });

        columnGitUser = createColumn(Columns.GIT_USER);
        columnGitUser.setLabelProvider(new ColumnLabelProvider() {

            @Override
            public String getText(Object element) {
                ElementMetaInfo meta = elementInfoMap.get(element);
                return meta != null ? meta.getGitUser() : EMPTY_STRING;
            }

            @Override
            public boolean isLabelProperty(Object element, String property) {
                return GITLABEL_PROP.equals(property);
            }
        });

        columnDbUser = createColumn(Columns.DB_USER);
        columnDbUser.setLabelProvider(new ColumnLabelProvider() {

            @Override
            public String getText(Object element) {
                ElementMetaInfo meta = elementInfoMap.get(element);
                return meta != null ? meta.getDbUser() : EMPTY_STRING;
            }
        });

        columnLocation = createColumn(Columns.LOCATION);
        columnLocation.setLabelProvider(new ColumnLabelProvider() {

            @Override
            public String getText(Object element) {
                return ((TreeElement) element).getContainerQName();
            }
        });

        setColumnHeaders();
        updateColumnsWidth();
    }

    private TreeViewerColumn createColumn(Columns columnType) {
        TreeViewerColumn column = new TreeViewerColumn(viewer, SWT.LEFT);
        column.getColumn().setResizable(true);
        column.getColumn().setMoveable(true);
        column.getColumn().addSelectionListener(getHeaderSelectionAdapter(columnType));
        column.getColumn().setToolTipText(Messages.DiffTableViewer_reset_sorting);

        return column;
    }

    private void setColumnHeaders(){
        columnCheck.getColumn().setText("✓"); //$NON-NLS-1$
        columnLibrary.getColumn().setText(EMPTY_STRING);
        columnName.getColumn().setText(Messages.diffTableViewer_object_name);
        columnType.getColumn().setText(Messages.diffTableViewer_object_type);
        columnChange.getColumn().setText(getChangeTypeMessage());
        columnLocation.getColumn().setText(Messages.diffTableViewer_container);
        columnGitUser.getColumn().setText(Messages.diffTableViewer_git_user);
        columnDbUser.getColumn().setText(Messages.diffTableViewer_db_user);
    }

    private String getChangeTypeMessage() {
        if (isApplyToProj) {
            return Messages.diffTableViewer_change_type_for_project;
        }

        return Messages.diffTableViewer_change_type_for_database;
    }

    private void updateColumnsWidth() {
        PixelConverter pc = new PixelConverter(viewer.getControl());
        columnCheck.getColumn().setWidth(viewOnly ? 0 : pc.convertWidthInCharsToPixels(9));
        columnLibrary.getColumn().setWidth(showLibrary && !viewOnly ? pc.convertWidthInCharsToPixels(5) : 0);
        columnType.getColumn().setWidth(pc.convertWidthInCharsToPixels(19));
        columnChange.getColumn().setWidth(pc.convertWidthInCharsToPixels(30));
        // name column will take third of the space
        int width = (int)(viewer.getControl().getSize().x * 0.35f);
        columnName.getColumn().setWidth(Math.max(width, 200));
        columnLocation.getColumn().setWidth(pc.convertWidthInCharsToPixels(20));
        columnGitUser.getColumn().setWidth(showGitUser && !viewOnly ? pc.convertWidthInCharsToPixels(20) : 0);
        columnDbUser.getColumn().setWidth(showDbUser && !viewOnly ? pc.convertWidthInCharsToPixels(20) : 0);
    }

    private SelectionAdapter getHeaderSelectionAdapter(final Columns index) {
        return new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                if ((e.stateMask & SWT.CTRL) != 0){
                    comparator.clearSortList();
                    setColumnHeaders();
                }
                sortViewer(index);
            }
        };
    }

    private void sortViewer(Columns index) {
        comparator.addSort(index);
        updateSortIndexes();
        viewer.refresh();
    }

    private void updateSortIndexes(){
        int i = 0;
        StringBuilder sb = new StringBuilder();
        for (SortingColumn col : comparator.sortOrder) {
            sb.setLength(0);
            sb.append(comparator.sortOrder.size() - i++)
            .append(!col.desc ? '\u25BF' : '\u25B5')
            .append('\t');

            switch (col.col) {
            case CHECK:
                sb.setLength(sb.length() - 1);
                columnCheck.getColumn().setText(sb.append('✓').toString());
                break;
            case LIBRARY:
                columnLibrary.getColumn().setText(sb.toString());
                break;
            case TYPE:
                columnType.getColumn().setText(sb.append(Messages.diffTableViewer_object_type).toString());
                break;
            case CHANGE:
                columnChange.getColumn().setText(sb.append(getChangeTypeMessage()).toString());
                break;
            case NAME:
                columnName.getColumn().setText(sb.append(Messages.diffTableViewer_object_name).toString());
                break;
            case LOCATION:
                columnLocation.getColumn().setText(sb.append(Messages.diffTableViewer_container).toString());
                break;
            case GIT_USER:
