/*******************************************************************************
 * Copyright (c) 2014, 2018 Red Hat Inc. and others.
 * 
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Red Hat - Initial Contribution
 *******************************************************************************/

package org.eclipse.linuxtools.internal.docker.ui.views;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

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.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.layout.TableColumnLayout;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.linuxtools.docker.core.DockerConnectionManager;
import org.eclipse.linuxtools.docker.core.EnumDockerConnectionState;
import org.eclipse.linuxtools.docker.core.IDockerConnection;
import org.eclipse.linuxtools.docker.core.IDockerConnectionManagerListener;
import org.eclipse.linuxtools.docker.core.IDockerContainer;
import org.eclipse.linuxtools.docker.core.IDockerImage;
import org.eclipse.linuxtools.docker.core.IDockerImageListener;
import org.eclipse.linuxtools.docker.ui.Activator;
import org.eclipse.linuxtools.internal.docker.ui.DockerConnectionWatcher;
import org.eclipse.linuxtools.internal.docker.ui.commands.CommandUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.forms.widgets.Form;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.views.properties.IPropertySheetPage;
import org.eclipse.ui.views.properties.tabbed.ITabbedPropertySheetPageContributor;
import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage;

public class DockerImagesView extends ViewPart implements IDockerImageListener,
		ISelectionListener,
		ITabbedPropertySheetPageContributor, IDockerConnectionManagerListener {

	/** Id of the view. */
	public static final String VIEW_ID = "org.eclipse.linuxtools.docker.ui.dockerImagesView";

	private static final String TOGGLE_STATE = "org.eclipse.ui.commands.toggleState"; //$NON-NLS-1$

	private static final String SHOW_ALL_IMAGES_COMMAND_ID = "org.eclipse.linuxtools.docker.ui.commands.showAllImages"; //$NON-NLS-1$
	private static final String SHOW_ALL_IMAGES_PREFERENCE = "showAllImages"; //$NON-NLS-1$

	private final static String NoConnectionSelected = "ViewerNoConnectionSelected.msg"; //$NON-NLS-1$
	private final static String ConnectionNotAvailable = "ViewerConnectionNotAvailable.msg"; //$NON-NLS-1$
	private final static String ViewAllTitle = "ImagesViewTitle.all.msg"; //$NON-NLS-1$
	private final static String ViewFilteredTitle = "ImagesViewTitle.filtered.msg"; //$NON-NLS-1$

	private Form form;
	private Text search;
	private TableViewer viewer;
	private IDockerConnection connection;
	private final DanglingImagesViewerFilter hideDanglingImagesFilter = new DanglingImagesViewerFilter();
	private final IntermediateImagesViewerFilter hideIntermediateImagesFilter = new IntermediateImagesViewerFilter();

	@Override
	public void setFocus() {
	}
	
	@Override
	public void dispose() {
		// remove this listener instance registered on the Docker connection
		if (connection != null) {
			connection.removeImageListener(this);
		}

		// stop tracking selection changes in the Docker Explorer view (only)
		getSite().getWorkbenchWindow().getSelectionService()
				.removeSelectionListener(DockerExplorerView.VIEW_ID, this);
		DockerConnectionManager.getInstance()
				.removeConnectionManagerListener(this);
		super.dispose();
	}

	@Override
	public String getContributorId() {
		return "org.eclipse.linuxtools.docker.ui.propertiesViewContributor"; //$NON-NLS-1$
	}

	/**
	 * @return the title of the form inside the view
	 */
	public String getFormTitle() {
		return this.form.getText();
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getAdapter(Class<T> adapter) {
		if (adapter == IPropertySheetPage.class) {
			return (T) new TabbedPropertySheetPage(this, true);
		}
		return super.getAdapter(adapter);
	}

	private void hookContextMenu() {
		MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$
		menuMgr.setRemoveAllWhenShown(true);
		// menuMgr.addMenuListener(new IMenuListener() {
		// public void menuAboutToShow(IMenuManager manager) {
		// DockerContainersView.this.fillContextMenu(manager);
		// }
		// });
		Menu menu = menuMgr.createContextMenu(viewer.getControl());
		viewer.getControl().setMenu(menu);
		getSite().registerContextMenu(menuMgr, viewer);
	}

	@Override
	public void createPartControl(final Composite parent) {
		final FormToolkit toolkit = new FormToolkit(parent.getDisplay());
		form = toolkit.createForm(parent);
		form.setText(DVMessages.getString(NoConnectionSelected));
		final Composite container = form.getBody();
		GridLayoutFactory.fillDefaults().numColumns(1).margins(0, 0).applyTo(container);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(container);
		createTableViewer(container);
		// track selection changes in the Docker Explorer view (only)
		getSite().getWorkbenchWindow().getSelectionService()
				.addSelectionListener(DockerExplorerView.VIEW_ID, this);
		hookContextMenu();
		// Look at stored preference to determine if all images should be
		// shown or just top-level images. By default, only show
		// top-level images.
		IEclipsePreferences preferences = InstanceScope.INSTANCE
				.getNode(Activator.PLUGIN_ID);
		boolean showAll = preferences.getBoolean(SHOW_ALL_IMAGES_PREFERENCE,
				false);
		showAllImages(showAll);
		final ICommandService service = getViewSite().getWorkbenchWindow()
				.getService(ICommandService.class);
		service.getCommand(SHOW_ALL_IMAGES_COMMAND_ID).getState(TOGGLE_STATE)
				.setValue(showAll);
		service.refreshElements(SHOW_ALL_IMAGES_COMMAND_ID, null);
		DockerConnectionManager.getInstance()
				.addConnectionManagerListener(this);
		// On startup, this view might get set up after the Docker Explorer View
		// and so we won't get the notification when it chooses the connection.
		// Find out if it has a selection and set our connection appropriately.
		ISelection selection = getSite().getWorkbenchWindow()
				.getSelectionService().getSelection(DockerExplorerView.VIEW_ID);
		if (selection != null) {
			selectionChanged(null, selection);
			// also notify DockerConnectionWatcher
			DockerConnectionWatcher.getInstance().selectionChanged(null,
					selection);
		}
	}
	
	private void createTableViewer(final Composite container) {
		search = new Text(container, SWT.SEARCH | SWT.ICON_SEARCH);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(search);
		search.addModifyListener(onSearch());
		Composite tableArea = new Composite(container, SWT.NONE);
		GridLayoutFactory.fillDefaults().numColumns(1).margins(0,  0).applyTo(tableArea);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL)
				.grab(true, true).applyTo(tableArea);

		final TableColumnLayout tableLayout = new TableColumnLayout();
		tableArea.setLayout(tableLayout);
		this.viewer = new TableViewer(tableArea, SWT.FULL_SELECTION | SWT.MULTI
				| SWT.H_SCROLL | SWT.V_SCROLL);
		this.viewer.setContentProvider(new DockerImagesContentProvider());
		final Table table = viewer.getTable();
		GridLayoutFactory.fillDefaults().numColumns(1).margins(0, 0)
				.applyTo(table);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL)
				.grab(true, true).applyTo(table);
		table.setLinesVisible(true);
		table.setHeaderVisible(true);
		// 'Image' column
		final TableViewerColumn idColumn = createColumn(DVMessages
				.getString("ID")); //$NON-NLS-1$
		setLayout(idColumn, tableLayout, 150);
		idColumn.setLabelProvider(new ColumnLabelProvider() {
			@Override
			public String getText(final Object element) {
				if (element instanceof IDockerImage) {
					// for image id, remove any sha256 digest prefix
					// and truncate to max of 12 characters
					String imageId = ((IDockerImage) element).id();
					if (imageId.startsWith("sha256:")) //$NON-NLS-1$
						imageId = imageId.substring(7);
					if (imageId.length() > 12) {
						return imageId.substring(0, 12);
					}
					return imageId;
				}
				return super.getText(element);
			}
		});
		// 'Repo/Tags' column
		final TableViewerColumn tagsColumn = createColumn(DVMessages
				.getString("TAGS")); //$NON-NLS-1$
		setLayout(tagsColumn, tableLayout, 150);
		tagsColumn.setLabelProvider(new ColumnLabelProvider() {
			@Override
			public String getText(final Object element) {
				if (element instanceof IDockerImage) {
					final IDockerImage image = (IDockerImage) element;
					final StringBuilder messageBuilder = new StringBuilder();
					for (Iterator<String> iterator = image.repoTags()
							.iterator(); iterator.hasNext();) {
						final String repoTag = iterator.next();
						messageBuilder.append(repoTag);
						if (iterator.hasNext()) {
							messageBuilder.append('\n');
						}
					}
					return messageBuilder.toString();
				}
				return super.getText(element);
			}
		});
		// 'Creation Date' column
		final TableViewerColumn creationDateColumn = createColumn(DVMessages
				.getString("CREATED")); //$NON-NLS-1$
		setLayout(creationDateColumn, tableLayout, 150);
		creationDateColumn.setLabelProvider(new ColumnLabelProvider() {
			@Override
			public String getText(final Object element) {
				if (element instanceof IDockerImage) {
					return LabelProviderUtils.toCreatedDate(Long
							.parseLong(((IDockerImage) element).created()));
				}
				return super.getText(element);
			}
		});
		// 'Virtual Size' column
		final TableViewerColumn virtsizeColumn = createColumn(DVMessages
				.getString("VIRTSIZE")); //$NON-NLS-1$ 
		setLayout(virtsizeColumn, tableLayout, 150);
		virtsizeColumn.setLabelProvider(new SpecialColumnLabelProvider() {
			@Override
			public String getText(final Object element) {
				if (element instanceof IDockerImage) {
					Long size = ((IDockerImage) element).virtualSize();
					if (size <= 0)
						return "0"; //$NON-NLS-1$
					final String[] units = new String[] { "B", "kB", "MB", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
							"GB", "TB" }; //$NON-NLS-1$ //$NON-NLS-2$
					int digitGroups = (int) (Math.log10(size) / Math
							.log10(1000));
					return new DecimalFormat("#,##0.#").format(size //$NON-NLS-1$
							/ Math.pow(1000, digitGroups))
							+ " " + units[digitGroups];
				}
				return super.getText(element);
			}

			@Override
			public String getCompareText(final Object element) {
				// For comparison purposes, we want to use the raw long value
				// and not the shortened value with units appended.
				if (element instanceof IDockerImage) {
					return new DecimalFormat("000000000000000000000000") //$NON-NLS-1$
							.format((((IDockerImage) element).virtualSize()));
				}
				return super.getText(element);
			}
		});
		// comparator
		final DockerImagesComparator comparator = new DockerImagesComparator(
				this.viewer);
		comparator.setColumn(creationDateColumn.getColumn());
		// Set column a second time so we reverse the order and default to most
		// currently created containers first
		comparator.setColumn(creationDateColumn.getColumn());
		viewer.setComparator(comparator);
		// apply search filter
		this.viewer.addFilter(getImagesFilter());
		setConnection(CommandUtils.getCurrentConnection(null));
		// get the current selection in the tableviewer
		getSite().setSelectionProvider(viewer);
	}

	private TableViewerColumn createColumn(final String title) {
		final TableViewerColumn propertyColumn = new TableViewerColumn(viewer,
				SWT.BORDER);
		propertyColumn.getColumn().setText(title);
		propertyColumn.getColumn().addSelectionListener(onColumnSelected());
		return propertyColumn;
	}
	
	private SelectionListener onColumnSelected() {
		return SelectionListener.widgetSelectedAdapter(e -> {
			final TableColumn sortColumn = (TableColumn) e.getSource();
			final DockerImagesComparator comparator = (DockerImagesComparator) viewer
					.getComparator();
			comparator.setColumn(sortColumn);
			viewer.refresh();
		});
	}

	private void setLayout(final TableViewerColumn viewerColumn, final TableColumnLayout tableLayout, final int weight) {
		tableLayout.setColumnData(viewerColumn.getColumn(), new ColumnWeightData(weight, true));
	}

	/**
	 * Filters {@link IDockerContainer} and {@link IDockerImage} using the input text in the search widget of this view.
	 * @return
	 */
	private ModifyListener onSearch() {
		return e -> {
			if (viewer != null) {
				viewer.refresh();
				refreshViewTitle();
			}
		};
	}
	
	/**
	 * @return a {@link ViewerFilter} that will retain {@link IDockerContainer} that match the 
	 * content of the {@link DockerContainerView#search} text widget.
	 */
	private ViewerFilter getImagesFilter() {
		return new ViewerFilter() {
			
			@Override
			public boolean select(Viewer viewer, Object parentElement, Object element) {
				// filtering Docker images
				if (element instanceof IDockerImage) {
					return element.toString().contains(DockerImagesView.this.search.getText());
				}
				// any other element should not make it through the filter (i.e., it is not displayed)
				return false;
			}
		};
	}
	
	@Override
	public void selectionChanged(IWorkbenchPart part, ISelection selection) {
		final ITreeSelection treeSelection = (ITreeSelection) selection;
		if(treeSelection.isEmpty()) {
			setConnection(null);
			return;
		}
		final Object firstSegment = treeSelection.getPaths()[0].getFirstSegment();
		if(firstSegment instanceof IDockerConnection) {
			final IDockerConnection connection = (IDockerConnection) firstSegment;
			setConnection(connection);
		}
	}
	
	@Override
	public void listChanged(final IDockerConnection connection,
			final List<IDockerImage> images) {
		if (connection.getName().equals(connection.getName())) {
			Display.getDefault().asyncExec(() -> {
				if (DockerImagesView.this.viewer != null
						&& !DockerImagesView.this.viewer.getTable()
								.isDisposed()) {
					DockerImagesView.this.viewer.refresh();
					refreshViewTitle();
				}
			});
		}
	}
	
	private void refreshViewTitle() {
		if (this.viewer == null || this.viewer.getControl().isDisposed()
				|| this.form == null
				|| this.connection == null) {
			return;
		} else if (this.connection
				.getState() == EnumDockerConnectionState.CLOSED) {
			this.form.setText(DVMessages.getFormattedString(
					ConnectionNotAvailable, connection.getName()));
			this.form.setEnabled(false);
		} else if (!this.connection.isImagesLoaded()) {
			this.form.setEnabled(false);
			this.form.setText(connection.getName());
		} else {
			final List<ViewerFilter> filters = Arrays
					.asList(this.viewer.getFilters());
			if (filters.contains(hideDanglingImagesFilter)
					|| filters.contains(hideIntermediateImagesFilter)) {
				this.form.setText(DVMessages.getFormattedString(
						ViewFilteredTitle, connection.getName(),
						Integer.toString(viewer.getTable().getItemCount()),
						Integer.toString(connection.getImages().size())));
			} else {
				this.form.setText(DVMessages.getFormattedString(ViewAllTitle,
						new String[] { connection.getName(), Integer
								.toString(connection.getImages().size()) }));

			}
			this.form.setEnabled(true);
		}
	}
	
	/**
	 * @return the {@link IDockerConnection} used to display the current {@link IDockerContainer}
	 */
	public IDockerConnection getConnection() {
		return connection;
	}

	private void setConnection(final IDockerConnection connection) {
		if (connection != null && connection.equals(this.connection)) {
			return;
		}
		if (this.connection != null) {
			this.connection.removeImageListener(this);
		}
		this.connection = connection;
		if (this.viewer != null && this.connection != null) {
			final Job refreshJob = new Job(
					DVMessages.getString("ImagesRefresh.msg")) {

				@Override
				protected IStatus run(IProgressMonitor monitor) {
					connection.getImages(true);
					connection.addImageListener(DockerImagesView.this);
					Display.getDefault().asyncExec(() -> {
						if (!viewer.getControl().isDisposed()) {
							viewer.setInput(connection);
							refreshViewTitle();
						}
					});
					return Status.OK_STATUS;
				}
			};
			refreshJob.schedule();
		} else if (this.viewer != null) {
			viewer.setInput(new IDockerContainer[0]);
			form.setText(DVMessages.getString(NoConnectionSelected));
		}
	}

	/**
	 * @return the current selection
	 */
	public IStructuredSelection getStructuredSelection() {
		if (this.viewer != null) {
			return this.viewer.getStructuredSelection();
		}
		return StructuredSelection.EMPTY;
	}

	/**
	 * @return the {@link TableViewer} showing the containers
	 */
	public TableViewer getViewer() {
		return this.viewer;
	}

	/**
	 * Activates {@link HideDanglingImagesFilter} and
	 * {@link HideIntermediateImagesFilter} if the given {@code enabled}
	 * argument is <code>false</code>, deactivates the filter otherwise.
	 * 
	 * @param enabled
	 *            the argument to enable/disable the filter.
	 */
	public void showAllImages(boolean enabled) {
		if(!enabled) {
			this.viewer.addFilter(hideDanglingImagesFilter);
			this.viewer.addFilter(hideIntermediateImagesFilter);
		} else {
			final List<ViewerFilter> filters = new ArrayList<>(
					Arrays.asList(this.viewer.getFilters()));
			// remove filters and make sure there is no duplicate in the list of
			// filters
			for (Iterator<ViewerFilter> iterator = filters.iterator(); iterator
					.hasNext();) {
				ViewerFilter viewerFilter = iterator.next();
				if (viewerFilter.equals(hideDanglingImagesFilter)
						|| viewerFilter.equals(hideIntermediateImagesFilter)) {
					iterator.remove();
				}
			}
			this.viewer.setFilters(filters.toArray(new ViewerFilter[0]));
		}
		// Save enablement across sessions using a preference variable.
		IEclipsePreferences preferences = InstanceScope.INSTANCE
				.getNode(Activator.PLUGIN_ID);
		preferences.putBoolean(SHOW_ALL_IMAGES_PREFERENCE, enabled);
		refreshViewTitle();
	}

	@Override
	public void changeEvent(final IDockerConnection connection,
			final int type) {
		if (type == IDockerConnectionManagerListener.UPDATE_SETTINGS_EVENT) {
			final Job refreshJob = new Job(
					DVMessages.getString("ImagesRefresh.msg")) {

				@Override
				protected IStatus run(IProgressMonitor monitor) {
					connection.getImages(true);
					return Status.OK_STATUS;
				}
			};
			refreshJob.addJobChangeListener(new JobChangeAdapter() {
				@Override
				public void done(IJobChangeEvent event) {
					Display.getDefault().asyncExec(() -> refreshViewTitle());
				}

			});
			refreshJob.schedule();
		} else if (type == IDockerConnectionManagerListener.RENAME_EVENT) {
			Display.getDefault().asyncExec(() -> refreshViewTitle());
		}
	}
}
