/*******************************************************************************
 * Copyright (c) 2015-2021 MINRES Technologies 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
 *
 * Contributors:
 *     MINRES Technologies GmbH - initial API and implementation
 *******************************************************************************/
package com.minres.scviewer.e4.application.internal.status;

import java.lang.ref.WeakReference;
import java.lang.reflect.Method;

import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.wb.swt.ResourceManager;
import org.eclipse.wb.swt.SWTResourceManager;
import org.osgi.service.prefs.Preferences;

import com.minres.scviewer.e4.application.Constants;

/**
 * The Heap Status control, which shows the heap usage statistics in the window
 * trim. Part of the code is taken from the eclipse internal implementation
 */
public class HeapStatus extends Composite {

	/** The armed. */
	private boolean armed;

	/** The gc image. */
	private Image gcImage;

	/** The disabled gc image. */
	private Image disabledGcImage;

	/** The arm col. */
	private Color bgCol;

	/** The used mem col. */
	private Color usedMemCol;

	/** The low mem col. */
	private Color lowMemCol;

	/** The free mem col. */
	private Color freeMemCol;

	/** The top left col. */
	private Color topLeftCol;

	/** The bottom right col. */
	private Color bottomRightCol;

	/** The sep col. */
	private Color sepCol;

	/** The text col. */
	private Color textCol;

	/** The mark col. */
	private Color markCol;

	/** The arm col. */
	private Color armCol;

	/** The button. */
	private Canvas button;

	/** The preferences. */
	private Preferences preferences;

	/** The update interval. */
	private int updateInterval;

	/** The show max. */
	private boolean showMax;

	/** The total mem. */
	private long totalMem;

	/** The prev total mem. */
	private long prevTotalMem = -1L;

	/** The prev used mem. */
	private long prevUsedMem = -1L;

	/** The has changed. */
	private boolean hasChanged;

	/** The used mem. */
	private long usedMem;

	/** The mark. */
	private long mark = -1;

	/** The img bounds. */
	// start with 12x12
	private Rectangle imgBounds = new Rectangle(0, 0, 12, 12);

	/** The max mem. */
	private long maxMem = Long.MAX_VALUE;

	/** The max mem known. */
	private boolean maxMemKnown;

	/** The low mem threshold. */
	private float lowMemThreshold = 0.05f;

	/** The show low mem threshold. */
	private boolean showLowMemThreshold = true;

	/** The update tooltip. */
	private boolean updateTooltip = false;

	/** The is in gc. */
	protected volatile boolean isInGC = false;

	/** The timer. */
	private final Runnable timer = new Runnable() {
		@Override
		public void run() {
			if (!isDisposed()) {
				updateStats();
				if (hasChanged) {
					if (updateTooltip) {
						updateToolTip();
					}
					redraw();
					hasChanged = false;
				}
				getDisplay().timerExec(updateInterval, this);
			}
		}
	};

	/** The pref listener. */
	private final IPreferenceChangeListener prefListener = event -> {
		if (IHeapStatusConstants.PREF_UPDATE_INTERVAL.equals(event.getKey())) {
			setUpdateIntervalInMS(preferences.getInt(IHeapStatusConstants.PREF_UPDATE_INTERVAL, 100));
		} else if (IHeapStatusConstants.PREF_SHOW_MAX.equals(event.getKey())) {
			showMax = preferences.getBoolean(IHeapStatusConstants.PREF_SHOW_MAX, true);
		}
	};

	/**
	 * Creates a new heap status control with the given parent, and using the given
	 * preference store to obtain settings such as the refresh interval.
	 *
	 * @param parent      the parent composite
	 * @param preferences the preference store
	 */
	public HeapStatus(Composite parent, Preferences preferences) {
		super(parent, SWT.NONE);

		maxMem = getMaxMem();
		maxMemKnown = maxMem != Long.MAX_VALUE;

		this.preferences = preferences;
		if (this.preferences instanceof IEclipsePreferences)
			((IEclipsePreferences) this.preferences).addPreferenceChangeListener(prefListener);

		setUpdateIntervalInMS(preferences.getInt(IHeapStatusConstants.PREF_UPDATE_INTERVAL, 100));
		showMax = preferences.getBoolean(IHeapStatusConstants.PREF_SHOW_MAX, true);

		button = new Canvas(this, SWT.NONE);
		button.setToolTipText("Run Garbage Collection");

		ImageDescriptor imageDesc = ResourceManager.getPluginImageDescriptor(Constants.PLUGIN_ID, "icons/trash.png"); //$NON-NLS-1$
		Display display = getDisplay();
		gcImage = imageDesc.createImage();
		if (gcImage != null) {
			imgBounds = gcImage.getBounds();
			disabledGcImage = new Image(display, gcImage, SWT.IMAGE_DISABLE);
		}
		usedMemCol = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND);
		lowMemCol = SWTResourceManager.getColor(255, 70, 70); // medium red
		freeMemCol = SWTResourceManager.getColor(255, 190, 125); // light orange
		bgCol = SWTResourceManager.getColor(SWT.COLOR_WIDGET_BACKGROUND);
		sepCol = topLeftCol = armCol = SWTResourceManager.getColor(SWT.COLOR_WIDGET_NORMAL_SHADOW);
		bottomRightCol = SWTResourceManager.getColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW);
		markCol = textCol = SWTResourceManager.getColor(SWT.COLOR_INFO_FOREGROUND);

		createContextMenu();

		Listener listener = event -> {
			switch (event.type) {
			case SWT.Dispose:
				doDispose();
				break;
			case SWT.Resize:
				Rectangle rect = getClientArea();
				button.setBounds(rect.width - imgBounds.width - 1, 1, imgBounds.width, rect.height - 2);
				break;
			case SWT.Paint:
				if (event.widget == HeapStatus.this) {
					paintComposite(event.gc);
				} else if (event.widget == button) {
					paintButton(event.gc);
				}
				break;
			case SWT.MouseUp:
				if (event.button == 1 && !isInGC) {
					arm(false);
					gc();
				}
				break;
			case SWT.MouseDown:
				if (event.button == 1) {
					if (event.widget == HeapStatus.this) {
						setMark();
					} else if (event.widget == button && !isInGC)
						arm(true);
				}
				break;
			case SWT.MouseEnter:
				HeapStatus.this.updateTooltip = true;
				updateToolTip();
				break;
			case SWT.MouseExit:
				if (event.widget == HeapStatus.this) {
					HeapStatus.this.updateTooltip = false;
				} else if (event.widget == button) {
					arm(false);
				}
				break;
			default:
				break;
			}
		};
		addListener(SWT.Dispose, listener);
		addListener(SWT.MouseDown, listener);
		addListener(SWT.Paint, listener);
		addListener(SWT.Resize, listener);
		addListener(SWT.MouseEnter, listener);
		addListener(SWT.MouseExit, listener);
		button.addListener(SWT.MouseDown, listener);
		button.addListener(SWT.MouseExit, listener);
		button.addListener(SWT.MouseUp, listener);
		button.addListener(SWT.Paint, listener);

		// make sure stats are updated before first paint
		updateStats();

		getDisplay().asyncExec(() -> {
			if (!isDisposed()) {
				getDisplay().timerExec(updateInterval, timer);
			}
		});
	}

	/**
	 * Sets the background.
	 *
	 * @param color the new background
	 */
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.swt.widgets.Control#setBackground(org.eclipse.swt.graphics.Color)
	 */
	@Override
	public void setBackground(Color color) {
		bgCol = color;
		button.redraw();
		button.update();
	}

	/**
	 * Sets the foreground.
	 *
	 * @param color the new foreground
	 */
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.swt.widgets.Control#setForeground(org.eclipse.swt.graphics.Color)
	 */
	@Override
	public void setForeground(Color color) {
		if (color == null) {
			usedMemCol = getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND);
		} else {
			usedMemCol = color;
		}

		button.redraw();
		button.update();
	}

	/**
	 * Gets the foreground.
	 *
	 * @return the foreground
	 */
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.swt.widgets.Control#getForeground()
	 */
	@Override
	public Color getForeground() {
		if (usedMemCol != null) {
			return usedMemCol;
		}
		return getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND);
	}

	/**
	 * Returns the maximum memory limit, or Long.MAX_VALUE if the max is not known.
	 *
	 * @return the max mem
	 */
	private long getMaxMem() {
		long max = Long.MAX_VALUE;
		try {
			// Must use reflect to allow compilation against JCL/Foundation
			Method maxMemMethod = Runtime.class.getMethod("maxMemory"); //$NON-NLS-1$
			Object o = maxMemMethod.invoke(Runtime.getRuntime());
			if (o instanceof Long) {
				max = ((Long) o).longValue();
			}
		} catch (Exception e) {
			// ignore if method missing or if there are other failures trying to determine
			// the max
		}
		return max;
	}

	/**
	 * Sets the update interval in ms.
	 *
	 * @param interval the new update interval in ms
	 */
	private void setUpdateIntervalInMS(int interval) {
		updateInterval = Math.max(100, interval);
	}

	/**
	 * Do dispose.
	 */
	private void doDispose() {
		if (preferences instanceof IEclipsePreferences)
			((IEclipsePreferences) preferences).removePreferenceChangeListener(prefListener);
		if (gcImage != null) {
			gcImage.dispose();
		}
		if (disabledGcImage != null) {
			disabledGcImage.dispose();
		}
	}

	/**
	 * Compute size.
	 *
	 * @param wHint the w hint
	 * @param hHint the h hint
	 * @param changed the changed
	 * @return the point
	 */
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.swt.widgets.Composite#computeSize(int, int, boolean)
	 */
	@Override
	public Point computeSize(int wHint, int hHint, boolean changed) {
		GC gc = new GC(this);
		Point p = gc.textExtent("MMMMMMMMMMMM");
		int height = imgBounds.height;
		// choose the largest of
		// - Text height + margins
		// - Image height + margins
		// - Default Trim heightin
		height = Math.max(height, p.y) + 4;
		height = Math.max(TrimUtil.TRIM_DEFAULT_HEIGHT, height);
		gc.dispose();
		return new Point(p.x + 15, height);
	}

	/**
	 * Arm.
	 *
	 * @param armed the armed
	 */
	private void arm(boolean armed) {
		if (this.armed == armed) {
			return;
		}
		this.armed = armed;
		button.redraw();
		button.update();
	}

	/**
	 * Gc running.
	 *
	 * @param isInGC the is in gc
	 */
	private void gcRunning(boolean isInGC) {
		if (this.isInGC == isInGC) {
			return;
		}
		this.isInGC = isInGC;
		button.redraw();
		button.update();
	}

	/**
	 * Creates the context menu.
	 */
	private void createContextMenu() {
		MenuManager menuMgr = new MenuManager();
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(mgr -> fillMenu(mgr));
		Menu menu = menuMgr.createContextMenu(this);
		setMenu(menu);
	}

	/**
	 * Fill menu.
	 *
	 * @param menuMgr the menu mgr
	 */
	private void fillMenu(IMenuManager menuMgr) {
		menuMgr.add(new SetMarkAction());
		menuMgr.add(new ClearMarkAction());
		menuMgr.add(new ShowMaxAction());
	}

	/**
	 * Sets the mark to the current usedMem level.
	 */
	private void setMark() {
		updateStats(); // get up-to-date stats before taking the mark
		mark = usedMem;
		hasChanged = true;
		redraw();
	}

	/**
	 * Clears the mark.
	 */
	private void clearMark() {
		mark = -1;
		hasChanged = true;
		redraw();
	}

	/**
	 * Gc.
	 */
	private void gc() {
		gcRunning(true);
		Thread t = new Thread() {
			@Override
			public void run() {
				busyGC();
				getDisplay().asyncExec(() -> {
					if (!isDisposed())
						gcRunning(false);
				});
			}
		};
		t.setDaemon(true);
		t.start();
	}

	/**
	 * Busy gc.
	 */
	private void busyGC() {
		Object obj = new Object();
		WeakReference<Object> ref = new WeakReference<>(obj);
		obj = null;
		do {
			System.gc();
			System.runFinalization();
		} while (ref.get() != null);
	}

	/**
	 * Paint button.
	 *
	 * @param gc the gc
	 */
	private void paintButton(GC gc) {
		Rectangle rect = button.getClientArea();
		if (isInGC) {
			if (disabledGcImage != null) {
				int buttonY = (rect.height - imgBounds.height) / 2 + rect.y;
				gc.drawImage(disabledGcImage, rect.x, buttonY);
			}
			return;
		}
		if (armed) {
			gc.setBackground(armCol);
			gc.fillRectangle(rect.x, rect.y, rect.width, rect.height);
		}
		if (gcImage != null) {
			int by = (rect.height - imgBounds.height) / 2 + rect.y; // button y
			gc.drawImage(gcImage, rect.x, by);
		}
	}

	/**
	 * Paint composite.
	 *
	 * @param gc the gc
	 */
	private void paintComposite(GC gc) {
		if (showMax && maxMemKnown) {
			paintCompositeMaxKnown(gc);
		} else {
			paintCompositeMaxUnknown(gc);
		}
	}

	/**
	 * Paint composite max unknown.
	 *
	 * @param gc the gc
	 */
	private void paintCompositeMaxUnknown(GC gc) {
		Rectangle rect = getClientArea();
		int x = rect.x;
		int y = rect.y;
		int w = rect.width;
		int h = rect.height;
		int bw = imgBounds.width; // button width
		int dx = x + w - bw - 2; // divider x
		int sw = w - bw - 3; // status width
		int uw = (int) (sw * usedMem / totalMem); // used mem width
		int ux = x + 1 + uw; // used mem right edge
		if (bgCol != null) {
			gc.setBackground(bgCol);
		}
		gc.fillRectangle(rect);
		gc.setForeground(sepCol);
		gc.drawLine(dx, y, dx, y + h);
		gc.drawLine(ux, y, ux, y + h);
		gc.setForeground(topLeftCol);
		gc.drawLine(x, y, x + w, y);
		gc.drawLine(x, y, x, y + h);
		gc.setForeground(bottomRightCol);
		gc.drawLine(x + w - 1, y, x + w - 1, y + h);
		gc.drawLine(x, y + h - 1, x + w, y + h - 1);

		gc.setBackground(usedMemCol);
		gc.fillRectangle(x + 1, y + 1, uw, h - 2);

		String s = convertToMegString(usedMem) + " of " + convertToMegString(totalMem);
		Point p = gc.textExtent(s);
		int sx = (rect.width - 15 - p.x) / 2 + rect.x + 1;
		int sy = (rect.height - 2 - p.y) / 2 + rect.y + 1;
		gc.setForeground(textCol);
		gc.drawString(s, sx, sy, true);

		// draw an I-shaped bar in the foreground colour for the mark (if present)
		if (mark != -1) {
			int ssx = (int) (sw * mark / totalMem) + x + 1;
			paintMark(gc, ssx, y, h);
		}
	}

	/**
	 * Paint composite max known.
	 *
	 * @param gc the gc
	 */
	private void paintCompositeMaxKnown(GC gc) {
		Rectangle rect = getClientArea();
		int x = rect.x;
		int y = rect.y;
		int w = rect.width;
		int h = rect.height;
		int bw = imgBounds.width; // button width
		int dx = x + w - bw - 2; // divider x
		int sw = w - bw - 3; // status width
		int uw = (int) (sw * usedMem / maxMem); // used mem width
		int ux = x + 1 + uw; // used mem right edge
		int tw = (int) (sw * totalMem / maxMem); // current total mem width
		int tx = x + 1 + tw; // current total mem right edge

		if (bgCol != null) {
			gc.setBackground(bgCol);
		}
		gc.fillRectangle(rect);
		gc.setForeground(sepCol);
		gc.drawLine(dx, y, dx, y + h);
		gc.drawLine(ux, y, ux, y + h);
		gc.drawLine(tx, y, tx, y + h);
		gc.setForeground(topLeftCol);
		gc.drawLine(x, y, x + w, y);
		gc.drawLine(x, y, x, y + h);
		gc.setForeground(bottomRightCol);
		gc.drawLine(x + w - 1, y, x + w - 1, y + h);
		gc.drawLine(x, y + h - 1, x + w, y + h - 1);

		if (lowMemThreshold != 0 && ((double) (maxMem - usedMem) / (double) maxMem < lowMemThreshold)) {
			gc.setBackground(lowMemCol);
		} else {
			gc.setBackground(usedMemCol);
		}
		gc.fillRectangle(x + 1, y + 1, uw, h - 2);

		gc.setBackground(freeMemCol);
		gc.fillRectangle(ux + 1, y + 1, tx - (ux + 1), h - 2);

		// paint line for low memory threshold
		if (showLowMemThreshold && lowMemThreshold != 0) {
			gc.setForeground(lowMemCol);
			int thresholdX = x + 1 + (int) (sw * (1.0 - lowMemThreshold));
			gc.drawLine(thresholdX, y + 1, thresholdX, y + h - 2);
		}

		String s = convertToMegString(usedMem) + " of " + convertToMegString(totalMem);
		Point p = gc.textExtent(s);
		int sx = (rect.width - 15 - p.x) / 2 + rect.x + 1;
		int sy = (rect.height - 2 - p.y) / 2 + rect.y + 1;
		gc.setForeground(textCol);
		gc.drawString(s, sx, sy, true);

		// draw an I-shaped bar in the foreground colour for the mark (if present)
		if (mark != -1) {
			int ssx = (int) (sw * mark / maxMem) + x + 1;
			paintMark(gc, ssx, y, h);
		}
	}

	/**
	 * Paint mark.
	 *
	 * @param gc the gc
	 * @param x  the x
	 * @param y  the y
	 * @param h  the h
	 */
	private void paintMark(GC gc, int x, int y, int h) {
		gc.setForeground(markCol);
		gc.drawLine(x, y + 1, x, y + h - 2);
		gc.drawLine(x - 1, y + 1, x + 1, y + 1);
		gc.drawLine(x - 1, y + h - 2, x + 1, y + h - 2);
	}

	/**
	 * Update stats.
	 */
	private void updateStats() {
		Runtime runtime = Runtime.getRuntime();
		totalMem = runtime.totalMemory();
		long freeMem = runtime.freeMemory();
		usedMem = totalMem - freeMem;

		if (convertToMeg(prevUsedMem) != convertToMeg(usedMem)) {
			prevUsedMem = usedMem;
			this.hasChanged = true;
		}

		if (prevTotalMem != totalMem) {
			prevTotalMem = totalMem;
			this.hasChanged = true;
		}
	}

	/**
	 * Update tool tip.
	 */
	private void updateToolTip() {
		String usedStr = convertToMegString(usedMem);
		String totalStr = convertToMegString(totalMem);
		String maxStr = maxMemKnown ? convertToMegString(maxMem) : "<unknown>";
		String markStr = mark == -1 ? "<none>" : convertToMegString(mark);
		String toolTip = "Heap size: " + usedStr + " of total: " + totalStr + " max: " + maxStr + " mark: " + markStr;
		if (!toolTip.equals(getToolTipText())) {
			setToolTipText(toolTip);
		}
	}

	/**
	 * Converts the given number of bytes to a printable number of megabytes
	 * (rounded up).
	 *
	 * @param numBytes the num bytes
	 * @return the string
	 */
	private String convertToMegString(long numBytes) {
		return Long.toString(convertToMeg(numBytes)) + "M";
	}

	/**
	 * Converts the given number of bytes to the corresponding number of megabytes
	 * (rounded up).
	 *
	 * @param numBytes the num bytes
	 * @return the long
	 */
	private long convertToMeg(long numBytes) {
		return (numBytes + (512 * 1024)) / (1024 * 1024);
	}

	/**
	 * The Class SetMarkAction.
	 */
	class SetMarkAction extends Action {

		/**
		 * Instantiates a new sets the mark action.
		 */
		SetMarkAction() {
			super("&Set Mark");
		}

		/**
		 * Run.
		 */
		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.jface.action.Action#run()
		 */
		@Override
		public void run() {
			setMark();
		}
	}

	/**
	 * The Class ClearMarkAction.
	 */
	class ClearMarkAction extends Action {

		/**
		 * Instantiates a new clear mark action.
		 */
		ClearMarkAction() {
			super("&Clear Mark");
		}

		/**
		 * Run.
		 */
		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.jface.action.Action#run()
		 */
		@Override
		public void run() {
			clearMark();
		}
	}

	/**
	 * The Class ShowMaxAction.
	 */
	class ShowMaxAction extends Action {

		/**
		 * Instantiates a new show max action.
		 */
		ShowMaxAction() {
			super("Show &Max Heap", IAction.AS_CHECK_BOX);
			setEnabled(maxMemKnown);
			setChecked(showMax);
		}

		/**
		 * Run.
		 */
		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.jface.action.Action#run()
		 */
		@Override
		public void run() {
			preferences.putBoolean(IHeapStatusConstants.PREF_SHOW_MAX, isChecked());
			redraw();
		}
	}
}
