/*******************************************************************************
 * Copyright (c) 2014, 2018 Red Hat, Inc.
 *
 * 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 API and implementation
 *******************************************************************************/
package org.eclipse.linuxtools.internal.gcov.view.annotatedsource;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.ui.CDTUITools;
import org.eclipse.cdt.ui.ICEditor;
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.IResourceDelta;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.IWindowListener;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.texteditor.ITextEditor;

/**
 * Keep track of windows/parts and listen for opened editors to add
 * the annotation model.
 */
public final class GcovAnnotationModelTracker {

    private static GcovAnnotationModelTracker single;
    private final IWorkbench workbench;
    private final Map<IProject, IPath> trackedProjects = new HashMap<>();

    /**
     * Add/Remove a part listener to every window open/closed.
     */
    private IWindowListener windowListener = new IWindowListener() {
        @Override
        public void windowOpened(IWorkbenchWindow window) {
            window.getPartService().addPartListener(partListener);
        }

        @Override
        public void windowClosed(IWorkbenchWindow window) {
            window.getPartService().removePartListener(partListener);
        }

        @Override
        public void windowActivated(IWorkbenchWindow window) {}

        @Override
        public void windowDeactivated(IWorkbenchWindow window) {}
    };

    /**
     * Add the GcovAnnotationModel to any part that contains an
     * instance of ICEditor.
     */
    private IPartListener2 partListener = new IPartListener2() {
        @Override
        public void partOpened(IWorkbenchPartReference partref) {
            if (partref != null) {
                annotateCEditor(partref);
            }
        }

        @Override
        public void partActivated(IWorkbenchPartReference partRef) {}

        @Override
        public void partBroughtToTop(IWorkbenchPartReference partRef) {}

        @Override
        public void partClosed(IWorkbenchPartReference partRef) {}

        @Override
        public void partDeactivated(IWorkbenchPartReference partRef) {}

        @Override
        public void partHidden(IWorkbenchPartReference partRef) {}

        @Override
        public void partVisible(IWorkbenchPartReference partRef) {}

        @Override
        public void partInputChanged(IWorkbenchPartReference partRef) {}
    };

    private GcovAnnotationModelTracker (IWorkbench workbench) {
        this.workbench = workbench;

        // Add part listener for current windows
        for (IWorkbenchWindow w : workbench.getWorkbenchWindows()) {
            w.getPartService().addPartListener(partListener);
        }

        // Add window listener to workbench for future windows
        workbench.addWindowListener(windowListener);
    }

    public static GcovAnnotationModelTracker getInstance () {
        if (single == null) {
            single = new GcovAnnotationModelTracker(PlatformUI.getWorkbench());
        }
        return single;
    }

    public IPath getBinaryPath (IProject project) {
        return trackedProjects.get(project);
    }

    public boolean containsProject (IProject project) {
        return trackedProjects.containsKey(project);
    }

    public void addProject (IProject project, IPath binary) {
        trackedProjects.put(project, binary);
    }

    public IProject[] getTrackedProjects() {
        return trackedProjects.keySet().toArray(new IProject[0]);
    }

    public void dispose() {
        workbench.removeWindowListener(windowListener);
        for (IWorkbenchWindow w : workbench.getWorkbenchWindows()) {
            w.getPartService().removePartListener(partListener);
        }
    }

    public void annotateAllCEditors() {
        for (IWorkbenchWindow w : workbench.getWorkbenchWindows()) {
            for (IWorkbenchPage p : w.getPages()) {
                for (IEditorReference e : p.getEditorReferences()) {
                    annotateCEditor(e);
                }
            }
        }
    }

    private void annotateCEditor(IWorkbenchPartReference partref) {
        IWorkbenchPart part = partref.getPart(false);
        if (part instanceof ICEditor) {
            ICEditor editor = (ICEditor) part;
            ICElement element = CDTUITools.getEditorInputCElement(editor.getEditorInput());
			if (element == null) {
				return;
			}
            IProject project = element.getCProject().getProject();

            // Attach our annotation model to any compatible editor. (ICEditor)
            GcovAnnotationModel.attach((ITextEditor) part);
            // If a user triggers a build we will not render annotations.
            ResourcesPlugin.getWorkspace().addResourceChangeListener(
                    new ProjectBuildListener(project, editor),
                    IResourceChangeEvent.POST_BUILD);
        }
    }


    private class ProjectBuildListener implements IResourceChangeListener {

        // project to keep track of
        private IProject project;
        private ICEditor editor;

        public ProjectBuildListener(IProject targetProject, ICEditor editor) {
            this.editor = editor;
            this.project = targetProject;
        }

        @Override
        public void resourceChanged(IResourceChangeEvent event) {
            if (project != null && isPostBuildEvent(event)) {

                // find the project from event delta and  delete its markers
                IResourceDelta delta = event.getDelta();
                IResourceDelta[] childrenDelta = delta.getAffectedChildren(IResourceDelta.CHANGED);
                for (IResourceDelta childDelta : childrenDelta) {
                    if (isProjectDelta(childDelta, project)) {

                        // do not track this project and de-register this listener
                        GcovAnnotationModel.clear(editor);
                        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
                        trackedProjects.remove(project);
                    }
                }
            }
        }

        /**
         * Check if {@link IResourceDelta} represents a change in the specified {@link IProject}..
         *
         * @param delta IResourceDelta resource delta to check
         * @param project IProject project to compare against
         * @return boolean true if IResourceDelta is a project and equals the
         */
        public boolean isProjectDelta(IResourceDelta delta, IProject project){
            if(delta != null){
                IResource resource = delta.getResource();
                return delta.getKind() == IResourceDelta.CHANGED
                        && resource != null
                        && resource.getType() == IResource.PROJECT
                        && resource.equals(project);
            }
            return false;
        }

        /**
         * Check if {@link IResourceChangeEvent} is a post-build event.
         *
         * @param event IResourceChangeEvent event to check
         * @return boolean true if IResourceChangeEvent is a post-build event, false
         *         otherwise
         */
        private boolean isPostBuildEvent(IResourceChangeEvent event) {
            if(event != null){
                int buildKind = event.getBuildKind();
                return  event.getType() == IResourceChangeEvent.POST_BUILD
                        && (buildKind == IncrementalProjectBuilder.FULL_BUILD
                        || buildKind == IncrementalProjectBuilder.INCREMENTAL_BUILD
                        || buildKind == IncrementalProjectBuilder.CLEAN_BUILD);
            }
            return false;
        }
    }
}
