/*******************************************************************************
 * Copyright (c) 2018 Remain Software
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     wim.jongman@remainsoftware.com - initial API and implementation
 *******************************************************************************/
package org.eclipse.tips.ide.internal;

import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer;
import org.eclipse.core.runtime.preferences.ConfigurationScope;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.jface.preference.PreferenceStore;
import org.eclipse.tips.core.internal.LogUtil;
import org.eclipse.tips.core.internal.TipManager;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.prefs.BackingStoreException;

/**
 * Internal class to store preferences.
 *
 * The preferences will first process command line options before accessing the
 * default scope which can be loaded with a plug-in Customization file.
 *
 * <h2>Control Startup</h2> Without any option, the tips will be started in the
 * background.
 * <p>
 * Command line option "org.eclipse.tips.startup.default" can be set in the
 * following ways:
 *
 * <ul>
 * <li><b>-Dorg.eclipse.tips.startup.default=dialog</b> (Show the dialog)</li>
 * <li><b>-Dorg.eclipse.tips.startup.default=background</b> (Start in the
 * background)</li>
 * <li><b>-Dorg.eclipse.tips.startup.default=disable</b> (Do not start
 * tips)</li>
 * </ul>
 *
 * The -pluginCustomization program option can be used to provide startup
 * defaults in a file:
 *
 * <pre>
 * <b>-pluginCustomization /some/path/plugin_customization.ini</b>
 *
 * Inside the file:
 * org.eclipse.tips.ide/activate_at_startup=&lt;dialog|background|disable&gt;
 * </pre>
 *
 * <h2>Disable specific providers</h2> Without any option, all providers will be
 * loaded.
 *
 * The -pluginCustomization program option can be used to disable specific
 * providers:
 *
 * <pre>
 * <b>-pluginCustomization /some/path/plugin_customization.ini</b>
 *
 * Inside the file:
 * org.eclipse.tips.ide/disabled_providers=org.eclipse.tips.ide.internal.provider.TipsTipProvider,another.provider.id
 * </pre>
 *
 */
@SuppressWarnings("restriction")
public class TipsPreferences extends AbstractPreferenceInitializer {

	private static final String FALSE = "false"; //$NON-NLS-1$

	/**
	 * Preference store key to indicate showing tips at startup.
	 */
	public static final String PREF_STARTUP_BEHAVIOR = "activate_at_startup"; //$NON-NLS-1$

	private static final String PREF_STARTUP_BEHAVIOR_PROPERTY = "org.eclipse.tips.startup.default"; //$NON-NLS-1$
	private static final String PREF_STARTUP_BEHAVIOR_DIALOG = "dialog"; //$NON-NLS-1$
	private static final String PREF_STARTUP_BEHAVIOR_BACKGROUND = "background"; //$NON-NLS-1$
	private static final String PREF_STARTUP_BEHAVIOR_DISABLE = "disable"; //$NON-NLS-1$

	/**
	 * Preference store key to indicate serving tips that the user as already seen.
	 */
	public static final String PREF_SERVE_READ_TIPS = "serve_read_tips"; //$NON-NLS-1$

	/**
	 * Preference store key to indicate providers which are disabled. I.e. no tips
	 * are shown from these providers. The providers are indicated by their
	 * identifiers, the identifiers are strings separated by commas.
	 */
	public static final String PREF_DISABLED_PROVIDERS = "disabled_providers"; //$NON-NLS-1$

	public TipsPreferences() {
	}

	@Override
	public void initializeDefaultPreferences() {
		// nothing needs to be done right now
	}

	/**
	 * Loads the read tips from disk.
	 *
	 * @return a map that stores the read tip hashes per provider.
	 */
	public static Map<String, List<Integer>> getReadState() {
		HashMap<String, List<Integer>> result = new HashMap<>();

		try {
			File stateLocation = getStateLocation();
			for (String key : stateLocation.list(getStateFileNameFilter(stateLocation))) {
				PreferenceStore store = new PreferenceStore(new File(stateLocation, key).getAbsolutePath());
				store.load();
				ArrayList<Integer> tips = new ArrayList<>();
				for (String tipKey : store.preferenceNames()) {
					if (!"provider".equals(tipKey)) { //$NON-NLS-1$
						tips.add(Integer.valueOf(store.getInt(tipKey)));
					}
				}
				result.put(store.getString("provider"), tips); //$NON-NLS-1$
			}
		} catch (Exception e) {
			Status status = new Status(IStatus.ERROR, "org.eclipse.tips.ide", e.getMessage(), e); //$NON-NLS-1$
			log(status);
		}
		return result;
	}

	private static FilenameFilter getStateFileNameFilter(File stateLocation) {
		return (pDir, pName) -> {
			if (pDir.equals(stateLocation) && pName.endsWith(".state")) { //$NON-NLS-1$
				return true;
			}
			return false;
		};
	}

	private static File getStateLocation() throws Exception {
		File file = new File(IDETipManager.getStateLocation(), "org.eclipse.tips.ide.state"); //$NON-NLS-1$
		if (!file.exists()) {
			file.mkdirs();
		}
		return file;
	}

	/**
	 * Saves the list with read tips to disk.
	 *
	 * @param pReadTips the list with read tips
	 * @return the status of the call
	 */
	public static IStatus saveReadState(Map<String, List<Integer>> pReadTips) {
		try {
			File stateLocation = getStateLocation();
			for (Map.Entry<String, List<Integer>> entry : pReadTips.entrySet()) {
				String child = entry.getKey();
				PreferenceStore store = new PreferenceStore(
						new File(stateLocation, child.trim() + ".state").getAbsolutePath()); //$NON-NLS-1$
				entry.getValue().forEach(value -> store.setValue(value.toString(), value.intValue()));
				store.setValue("provider", child); //$NON-NLS-1$
				store.save();
			}
			return Status.OK_STATUS;
		} catch (Exception e) {
			e.printStackTrace();
			return new Status(IStatus.ERROR, "org.eclipse.tips.ide", e.getMessage(), e); //$NON-NLS-1$
		}
	}

	/**
	 * @return the preferences for this bundle
	 */
	private static IEclipsePreferences getPreferences() {
		return ConfigurationScope.INSTANCE.getNode(Constants.BUNDLE_ID);

	}

	public static void log(IStatus status) {
		if (status.matches(IStatus.ERROR | IStatus.WARNING) || isDebug()) {
			Bundle bundle = FrameworkUtil.getBundle(TipsPreferences.class);
			Platform.getLog(bundle).log(status);
		}
		if (isConsoleLog()) {
			System.out.println(
					String.format("%1$tR:%1$tS:%1$tN - %2$s", Calendar.getInstance().getTime(), format(status))); //$NON-NLS-1$
		}
	}

	/**
	 * Converts the passed status object to a readable string and tries to get the
	 * location where the log method was called.
	 *
	 * @param pStatus the status to format
	 * @return the formatted status to string.
	 */
	private static Object format(IStatus pStatus) {
		StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
		String origin = "<unknown>"; //$NON-NLS-1$
		if (stackTrace.length > 3) {
			StackTraceElement ste = stackTrace[4];
			String[] fqcn = ste.getClassName().split("\\."); //$NON-NLS-1$
			String clazz = fqcn[fqcn.length - 1];
			origin = clazz + "#" + ste.getMethodName() + "(" + ste.getLineNumber() + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}

		String statusLine = pStatus.toString();
		if (statusLine.endsWith(" null")) { //$NON-NLS-1$
			statusLine = statusLine.substring(0, statusLine.length() - " null".length()); //$NON-NLS-1$
		}
		return statusLine + " : " + origin; //$NON-NLS-1$
	}

	/**
	 * Preferences are stored as strings but returned in TipManager form (int).
	 *
	 * @return the startup behavior
	 *
	 * @see TipManager#START_DIALOG
	 * @see TipManager#START_BACKGROUND
	 * @see TipManager#START_DISABLE
	 */
	public static int getStartupBehavior() {
		String behavior = getPreferences().get(PREF_STARTUP_BEHAVIOR, null);
		if (behavior == null) {
			behavior = getDefaultStartupBehavior();
		}
		switch (behavior) {
		case PREF_STARTUP_BEHAVIOR_DIALOG:
			return TipManager.START_DIALOG;
		case PREF_STARTUP_BEHAVIOR_BACKGROUND:
			return TipManager.START_BACKGROUND;
		case PREF_STARTUP_BEHAVIOR_DISABLE:
			return TipManager.START_DISABLE;
		default:
			return TipManager.START_BACKGROUND;
		}
	}

	private static String getDefaultStartupBehavior() {
		String startupBehavior = System.getProperty(PREF_STARTUP_BEHAVIOR_PROPERTY);
		if (startupBehavior == null) {
			return startupBehavior = DefaultScope.INSTANCE.getNode(Constants.BUNDLE_ID).get(PREF_STARTUP_BEHAVIOR,
					PREF_STARTUP_BEHAVIOR_BACKGROUND);
		}
		return startupBehavior;
	}

	public static boolean isServeReadTips() {
		return getPreferences().getBoolean(PREF_SERVE_READ_TIPS, false);
	}

	/**
	 * Sets the startup behavior of the tips framework which is one of the
	 * TipManager#START* constants. If an invalid value is passed,
	 * {@link TipManager#START_BACKGROUND} is assumed.
	 *
	 * @param startupBehavior {@link TipManager#START_BACKGROUND},
	 *                        {@link TipManager#START_DIALOG} or
	 *                        {@link TipManager#START_DISABLE}
	 */
	public static void setStartupBehavior(int startupBehavior) {
		log(LogUtil.info("setStartupBehavior = '" + startupBehavior + "' 0=dialog, 1=background, 2=disable")); //$NON-NLS-1$ //$NON-NLS-2$
		IEclipsePreferences node = getPreferences();
		if (startupBehavior == TipManager.START_DIALOG) {
			node.put(PREF_STARTUP_BEHAVIOR, PREF_STARTUP_BEHAVIOR_DIALOG);
		} else if (startupBehavior == TipManager.START_DISABLE) {
			node.put(PREF_STARTUP_BEHAVIOR, PREF_STARTUP_BEHAVIOR_DISABLE);
		} else {
			node.put(PREF_STARTUP_BEHAVIOR, PREF_STARTUP_BEHAVIOR_BACKGROUND);
		}
		try {
			node.flush();
		} catch (BackingStoreException e) {
			log(LogUtil.error(e));
		}
	}

	public static void setServeReadTips(boolean serveReadTips) {
		log(LogUtil.info("Entering method setServeReadTips with boolean parameter = '" + serveReadTips + "'")); //$NON-NLS-1$ //$NON-NLS-2$
		IEclipsePreferences node = getPreferences();
		node.putBoolean(PREF_SERVE_READ_TIPS, serveReadTips);
		try {
			node.flush();
		} catch (BackingStoreException e) {
			log(LogUtil.error(e));
		}
	}

	/**
	 * @return true if tips are in debug mode.
	 */
	public static boolean isDebug() {
		return !System.getProperty("org.eclipse.tips.debug", FALSE).equals(FALSE); //$NON-NLS-1$
	}

	/**
	 * @return true if console logging is required
	 */
	public static boolean isConsoleLog() {
		return !System.getProperty("org.eclipse.tips.consolelog", FALSE).equals(FALSE); //$NON-NLS-1$
	}

	/**
	 * @return A list of tip provider identifiers, which are disabled with a
	 *         preference on the configuration scope.
	 */
	public static List<String> getDisabledProviderIds() {
		String disabledProviderIds = getPreferences().get(PREF_DISABLED_PROVIDERS, null);
		if (disabledProviderIds == null) {
			disabledProviderIds = getDefaultDisabledProviderIds();
		}
		log(LogUtil.info("Disabled provider ids = '" + disabledProviderIds + "'")); //$NON-NLS-1$ //$NON-NLS-2$
		String[] result = disabledProviderIds.split(","); //$NON-NLS-1$
		return Arrays.asList(result);
	}

	/**
	 * @return A list of default tip provider identifiers, which can be provided by
	 *         a pluginCustomization file, or an empty string. Never null.
	 */
	private static String getDefaultDisabledProviderIds() {
		String result = Platform.getPreferencesService().getString(Constants.BUNDLE_ID, PREF_DISABLED_PROVIDERS, "", //$NON-NLS-1$
				new IScopeContext[] { DefaultScope.INSTANCE });
		log(LogUtil.info("Default disabled provider ids = '" + result + "'")); //$NON-NLS-1$ //$NON-NLS-2$
		return result;
	}
}