/*******************************************************************************
 * Copyright (c) 2004, 2011 QNX Software Systems and others.
 *
 * 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:
 *     QNX Software Systems - initial API and implementation
 *     IBM Corporation
 *******************************************************************************/
package org.eclipse.cdt.make.internal.core.scannerconfig;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.PathEntryContainerChanged;
import org.eclipse.cdt.make.core.MakeCorePlugin;
import org.eclipse.cdt.make.core.scannerconfig.IDiscoveredPathManager;
import org.eclipse.cdt.make.core.scannerconfig.IScannerConfigBuilderInfo2;
import org.eclipse.cdt.make.core.scannerconfig.IScannerConfigBuilderInfo2Set;
import org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector;
import org.eclipse.cdt.make.core.scannerconfig.IScannerInfoCollector2;
import org.eclipse.cdt.make.core.scannerconfig.InfoContext;
import org.eclipse.cdt.make.core.scannerconfig.ScannerConfigScope;
import org.eclipse.cdt.make.internal.core.MakeMessages;
import org.eclipse.cdt.make.internal.core.scannerconfig2.SCProfileInstance;
import org.eclipse.cdt.make.internal.core.scannerconfig2.ScannerConfigProfileManager;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;

public class DiscoveredPathManager implements IDiscoveredPathManager, IResourceChangeListener {

	private Map<IProject, DiscoveredInfoHolder> fDiscoveredInfoHolderMap = new HashMap<>();
	private List<IDiscoveredInfoListener> listeners = Collections
			.synchronizedList(new ArrayList<IDiscoveredInfoListener>());

	private static final int INFO_CHANGED = 1;
	private static final int INFO_REMOVED = 2;

	private static class DiscoveredInfoHolder {
		Map<InfoContext, IDiscoveredPathInfo> fInfoMap = new HashMap<>();
		//		PathSettingsContainer fContainer = PathSettingsContainer.createRootContainer();

		public IDiscoveredPathInfo getInfo(InfoContext context) {
			return fInfoMap.get(context);
		}

		//		private Map getMap(IPath path, boolean create, boolean exactPath){
		//			PathSettingsContainer child = fContainer.getChildContainer(path, create, exactPath);
		//			Map map = null;
		//			if(child != null){
		//				map = (Map)child.getValue();
		//				if(map == null && create){
		//					map = new HashMap();
		//					child.setValue(map);
		//				}
		//			}
		//
		//			return map;
		//		}

		//		public IDiscoveredPathInfo getInfo(IFile file, String instanceId){
		//			IPath path = file.getProjectRelativePath();
		//			Map map = getMap(path, false, false);
		//			for(Iterator iter = map.entrySet().iterator(); iter.hasNext();){
		//				Map.Entry entry = (Map.Entry)iter.next();
		//				InfoContext context = (InfoContext)entry.getKey();
		//				if(context.matches(file))
		//					return (IDiscoveredPathInfo)entry.getValue();
		//			}
		//			return null;
		//		}

		public IDiscoveredPathInfo setInfo(InfoContext context, IDiscoveredPathInfo info) {
			if (info != null)
				return fInfoMap.put(context, info);
			return fInfoMap.remove(context);
		}

	}

	public DiscoveredPathManager() {

	}

	public void startup() {
		ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
	}

	public void shutdown() {
		ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
	 */
	@Override
	public void resourceChanged(IResourceChangeEvent event) {
		if (event.getSource() instanceof IWorkspace) {
			IResource resource = event.getResource();

			switch (event.getType()) {
			case IResourceChangeEvent.POST_CHANGE:
				DiscoveredScannerInfoStore.getInstance().updateScannerConfigStore(event.getDelta());
				break;
			case IResourceChangeEvent.PRE_DELETE:
			case IResourceChangeEvent.PRE_CLOSE:
				if (resource.getType() == IResource.PROJECT) {
					//TODO: better handlind of resource remove/rename
					fDiscoveredInfoHolderMap.remove(resource);
					ScannerConfigProfileManager.getInstance().handleProjectRemoved(resource.getProject());
				}
				break;
			}
		}
	}

	@Override
	public IDiscoveredPathInfo getDiscoveredInfo(IProject project) throws CoreException {
		return getDiscoveredInfo(project, new InfoContext(project));
	}

	@Override
	public IDiscoveredPathInfo getDiscoveredInfo(IProject project, InfoContext context) throws CoreException {
		return getDiscoveredInfo(project, context, true);
	}

	@Override
	public IDiscoveredPathInfo getDiscoveredInfo(IProject project, InfoContext context,
			boolean defaultToProjectSettings) throws CoreException {
		DiscoveredInfoHolder holder = getHolder(project, true);
		IDiscoveredPathInfo info = holder.getInfo(context);

		if (info == null) {
			info = loadPathInfo(project, context, defaultToProjectSettings);
			holder.setInfo(context, info);
		}

		return info;
	}

	private DiscoveredInfoHolder getHolder(IProject project, boolean create) {
		DiscoveredInfoHolder holder = fDiscoveredInfoHolderMap.get(project);
		if (holder == null && create) {
			holder = new DiscoveredInfoHolder();
			fDiscoveredInfoHolderMap.put(project, holder);
		}
		return holder;
	}

	private IDiscoveredPathInfo loadPathInfo(IProject project, InfoContext context, boolean defaultToProjectSettings)
			throws CoreException {
		IDiscoveredPathInfo pathInfo = null;

		IScannerConfigBuilderInfo2Set container = ScannerConfigProfileManager.createScannerConfigBuildInfo2Set(project);
		IScannerConfigBuilderInfo2 buildInfo = container.getInfo(context);
		if (buildInfo == null && defaultToProjectSettings)
			buildInfo = container.getInfo(new InfoContext(project));

		if (buildInfo != null) {
			String profileId = buildInfo.getSelectedProfileId();
			SCProfileInstance profileInstance = ScannerConfigProfileManager.getInstance().getSCProfileInstance(project,
					context, profileId);
			IScannerInfoCollector collector = profileInstance.getScannerInfoCollector();

			if (collector instanceof IScannerInfoCollector2) {
				IScannerInfoCollector2 collector2 = (IScannerInfoCollector2) collector;
				pathInfo = collector2.createPathInfoObject();
			}
		}
		if (pathInfo == null) {
			pathInfo = new DiscoveredPathInfo(project);
		}
		return pathInfo;
	}

	//	private DiscoveredInfoHolder getHolder

	//	private IDiscoveredPathInfo loadPathInfo(IProject project) throws CoreException {
	//        IDiscoveredPathInfo pathInfo = null;
	//
	//        IScannerConfigBuilderInfo2 buildInfo = ScannerConfigProfileManager.createScannerConfigBuildInfo2(project);
	//        String profileId = buildInfo.getSelectedProfileId();
	//        SCProfileInstance profileInstance = ScannerConfigProfileManager.getInstance().
	//                getSCProfileInstance(project, profileId);
	//        IScannerInfoCollector collector = profileInstance.getScannerInfoCollector();
	//
	//        if (collector instanceof IScannerInfoCollector2) {
	//            IScannerInfoCollector2 collector2 = (IScannerInfoCollector2) collector;
	//            pathInfo = collector2.createPathInfoObject();
	//        }
	//        else {
	//            pathInfo = new DiscoveredPathInfo(project);
	//        }
	//		return pathInfo;
	//	}

	@Override
	public void removeDiscoveredInfo(IProject project) {
		removeDiscoveredInfo(project, new InfoContext(project));
	}

	@Override
	public void removeDiscoveredInfo(IProject project, InfoContext context) {
		DiscoveredInfoHolder holder = getHolder(project, false);
		if (holder != null) {
			IDiscoveredPathInfo info = holder.setInfo(context, null);
			if (info != null) {
				fireUpdate(INFO_REMOVED, info);
			}
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IDiscoveredPathManager#updateDiscoveredInfo(org.eclipse.cdt.make.core.scannerconfig.IDiscoveredPathManager.IDiscoveredPathInfo, java.util.List)
	 */
	@Override
	public void updateDiscoveredInfo(IDiscoveredPathInfo info, List<IResource> changedResources) throws CoreException {
		updateDiscoveredInfo(new InfoContext(info.getProject()), info, true, changedResources);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IDiscoveredPathManager#updateDiscoveredInfo(org.eclipse.cdt.make.core.scannerconfig.IDiscoveredPathManager.IDiscoveredPathInfo, java.util.List)
	 */
	@Override
	public void updateDiscoveredInfo(InfoContext context, IDiscoveredPathInfo info, boolean updateContainer,
			List<IResource> changedResources) throws CoreException {
		DiscoveredInfoHolder holder = getHolder(info.getProject(), true);
		IDiscoveredPathInfo oldInfo = holder.getInfo(context);
		if (oldInfo != null) {
			IDiscoveredScannerInfoSerializable serializable = info.getSerializable();
			if (serializable != null) {
				holder.setInfo(context, info);
				IProject project = info.getProject();
				DiscoveredScannerInfoStore.getInstance().saveDiscoveredScannerInfoToState(project, context,
						serializable);
				fireUpdate(INFO_CHANGED, info);

				if (updateContainer) {

					IScannerConfigBuilderInfo2 buildInfo = ScannerConfigProfileManager
							.createScannerConfigBuildInfo2(project);
					String profileId = buildInfo.getSelectedProfileId();
					ScannerConfigScope profileScope = ScannerConfigProfileManager.getInstance()
							.getSCProfileConfiguration(profileId).getProfileScope();
					changeDiscoveredContainer(project, profileScope, changedResources);
				}
			} else {
				throw new CoreException(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), -1,
						MakeMessages.getString("DiscoveredPathManager.Info_Not_Serializable"), null)); //$NON-NLS-1$
			}
		}
	}

	/**
	 * Allows one to update the discovered information for a particular scanner discovery profile ID.
	 * TODO:  This should be made API in IDiscoveredPathManager, or in an interface derived there from.
	 *
	 * @param context
	 * @param info
	 * @param updateContainer
	 * @param changedResources
	 * @param profileId
	 * @throws CoreException
	 */
	public void updateDiscoveredInfo(InfoContext context, IDiscoveredPathInfo info, boolean updateContainer,
			List<IResource> changedResources, String profileId) throws CoreException {
		DiscoveredInfoHolder holder = getHolder(info.getProject(), true);
		IDiscoveredPathInfo oldInfo = holder.getInfo(context);
		if (oldInfo != null) {
			IDiscoveredScannerInfoSerializable serializable = info.getSerializable();
			if (serializable != null) {
				holder.setInfo(context, info);
				IProject project = info.getProject();
				DiscoveredScannerInfoStore.getInstance().saveDiscoveredScannerInfoToState(project, context,
						serializable);
				fireUpdate(INFO_CHANGED, info);

				if (updateContainer) {

					IScannerConfigBuilderInfo2 buildInfo = ScannerConfigProfileManager
							.createScannerConfigBuildInfo2(project);

					ScannerConfigScope profileScope = ScannerConfigProfileManager.getInstance()
							.getSCProfileConfiguration(profileId).getProfileScope();
					changeDiscoveredContainer(project, profileScope, changedResources);
				}
			} else {
				throw new CoreException(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), -1,
						MakeMessages.getString("DiscoveredPathManager.Info_Not_Serializable"), null)); //$NON-NLS-1$
			}
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.make.core.scannerconfig.IDiscoveredPathManager#changeDiscoveredContainer(org.eclipse.core.resources.IProject, java.lang.String)
	 */
	@Override
	public void changeDiscoveredContainer(final IProject project, final ScannerConfigScope profileScope,
			final List<IResource> changedResources) {
		// order here is of essence
		// 1. clear DiscoveredPathManager's path info cache
		DiscoveredInfoHolder holder = getHolder(project, false);
		InfoContext context = new InfoContext(project);
		IDiscoveredPathInfo oldInfo = holder.getInfo(context);

		// 2. switch the containers
		try {
			IWorkspaceRunnable runnable = new IWorkspaceRunnable() {
				@Override
				public void run(IProgressMonitor monitor) throws CoreException {
					ICProject cProject = CoreModel.getDefault().create(project);
					if (ScannerConfigScope.PROJECT_SCOPE.equals(profileScope)) {
						CoreModel.setPathEntryContainer(new ICProject[] { cProject },
								new DiscoveredPathContainer(project), null);
					} else if (ScannerConfigScope.FILE_SCOPE.equals(profileScope)) {
						PerFileDiscoveredPathContainer container = new PerFileDiscoveredPathContainer(project);
						CoreModel.setPathEntryContainer(new ICProject[] { cProject }, container, null);
						if (changedResources != null) {
							List<PathEntryContainerChanged> changeDelta = new ArrayList<>(changedResources.size());
							for (IResource resource : changedResources) {
								IPath path = resource.getFullPath();
								changeDelta.add(
										new PathEntryContainerChanged(path, PathEntryContainerChanged.INCLUDE_CHANGED
												| PathEntryContainerChanged.MACRO_CHANGED)); // both include paths and symbols changed
							}
							CoreModel.pathEntryContainerUpdates(container,
									changeDelta.toArray(new PathEntryContainerChanged[changeDelta.size()]), null);
						}
					} else {
						MakeCorePlugin.log(new Status(IStatus.ERROR, MakeCorePlugin.getUniqueIdentifier(), 1,
								MakeMessages.getString("DiscoveredContainer.ScopeErrorMessage"), null)); //$NON-NLS-1$
					}

				}
			};
			CoreModel.run(runnable, null);
		} catch (CoreException e) {
			MakeCorePlugin.log(e);
		}

		// 3. clear the container's path entry cache
		if (oldInfo != null) {
			fireUpdate(INFO_REMOVED, oldInfo);
		}
	}

	private void fireUpdate(final int type, final IDiscoveredPathInfo info) {
		Object[] list = listeners.toArray();
		for (int i = 0; i < list.length; i++) {
			final IDiscoveredInfoListener listener = (IDiscoveredInfoListener) list[i];
			if (listener != null) {
				SafeRunner.run(new ISafeRunnable() {

					@Override
					public void handleException(Throwable exception) {
						IStatus status = new Status(IStatus.ERROR, CCorePlugin.PLUGIN_ID, -1,
								CCorePlugin.getResourceString("CDescriptorManager.exception.listenerError"), exception); //$NON-NLS-1$
						CCorePlugin.log(status);
					}

					@Override
					public void run() throws Exception {
						switch (type) {
						case INFO_CHANGED:
							listener.infoChanged(info);
							break;
						case INFO_REMOVED:
							listener.infoRemoved(info);
							break;
						}
					}
				});
			}
		}
	}

	@Override
	public void addDiscoveredInfoListener(IDiscoveredInfoListener listener) {
		listeners.add(listener);
	}

	@Override
	public void removeDiscoveredInfoListener(IDiscoveredInfoListener listener) {
		listeners.remove(listener);
	}
}
