/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Apache License, Version 2.0. A 
 * copy of the license can be found in the License.html file at the root of this distribution. If 
 * you cannot locate the Apache License, Version 2.0, please send an email to 
 * vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Apache License, Version 2.0.
 *
 * You must not remove this notice, or any other, from this software.
 *
 * ***************************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics.Contracts;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Project;

namespace Microsoft.VisualStudio.Navigation {

    /// <summary>
    /// Inplementation of the service that builds the information to expose to the symbols
    /// navigation tools (class view or object browser) from the source files inside a
    /// hierarchy.
    /// </summary>
    public abstract partial class LibraryManager : IDisposable, IVsRunningDocTableEvents {
        private readonly CommonPackage/*!*/ _package;
        private readonly Dictionary<uint, TextLineEventListener> _documents;
        private readonly Dictionary<IVsHierarchy, HierarchyListener> _hierarchies;
        private readonly Dictionary<ModuleId, LibraryNode> _files;
        private readonly Library _library;
        private readonly IVsEditorAdaptersFactoryService _adapterFactory;
        private uint _objectManagerCookie;
        private uint _runningDocTableCookie;
        
        public LibraryManager(CommonPackage/*!*/ package) {
            Contract.Assert(package != null);
            _package = package;
            _documents = new Dictionary<uint, TextLineEventListener>();
            _hierarchies = new Dictionary<IVsHierarchy, HierarchyListener>();
            _library = new Library(new Guid(CommonConstants.LibraryGuid));
            _library.LibraryCapabilities = (_LIB_FLAGS2)_LIB_FLAGS.LF_PROJECT;
            _files = new Dictionary<ModuleId, LibraryNode>();

            var model = ((IServiceContainer)package).GetService(typeof(SComponentModel)) as IComponentModel;
            _adapterFactory = model.GetService<IVsEditorAdaptersFactoryService>();

            // Register our library now so it'll be available for find all references
            RegisterLibrary();
        }

        public Library Library {
            get { return _library; }
        }

        protected abstract LibraryNode CreateLibraryNode(IScopeNode subItem, string namePrefix, IVsHierarchy hierarchy, uint itemid);
        public virtual LibraryNode CreateFileLibraryNode(HierarchyNode hierarchy, string name, string filename, LibraryNodeType libraryNodeType) {
            return new LibraryNode(name, filename, libraryNodeType);
        }
       

        private object GetPackageService(Type/*!*/ type) {
            return ((System.IServiceProvider)_package).GetService(type);
        }

        private void RegisterForRDTEvents() {
            if (0 != _runningDocTableCookie) {
                return;
            }
            IVsRunningDocumentTable rdt = GetPackageService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable;
            if (null != rdt) {
                // Do not throw here in case of error, simply skip the registration.
                rdt.AdviseRunningDocTableEvents(this, out _runningDocTableCookie);
            }
        }

        private void UnregisterRDTEvents() {
            if (0 == _runningDocTableCookie) {
                return;
            }
            IVsRunningDocumentTable rdt = GetPackageService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable;
            if (null != rdt) {
                // Do not throw in case of error.
                rdt.UnadviseRunningDocTableEvents(_runningDocTableCookie);
            }
            _runningDocTableCookie = 0;
        }

        #region ILibraryManager Members

        public void RegisterHierarchy(IVsHierarchy hierarchy) {
            if ((null == hierarchy) || _hierarchies.ContainsKey(hierarchy)) {
                return;
            }
            
            RegisterLibrary();
        
            HierarchyListener listener = new HierarchyListener(hierarchy, this);
            listener.StartListening(true);
            _hierarchies.Add(hierarchy, listener);
            RegisterForRDTEvents();
        }

        private void RegisterLibrary() {
            if (0 == _objectManagerCookie) {
                IVsObjectManager2 objManager = GetPackageService(typeof(SVsObjectManager)) as IVsObjectManager2;
                if (null == objManager) {
                    return;
                }
                Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(
                    objManager.RegisterSimpleLibrary(_library, out _objectManagerCookie));
            }
        }

        public void UnregisterHierarchy(IVsHierarchy hierarchy) {
            if ((null == hierarchy) || !_hierarchies.ContainsKey(hierarchy)) {
                return;
            }
            HierarchyListener listener = _hierarchies[hierarchy];
            if (null != listener) {
                listener.Dispose();
            }
            _hierarchies.Remove(hierarchy);
            if (0 == _hierarchies.Count) {
                UnregisterRDTEvents();
            }
            lock (_files) {
                ModuleId[] keys = new ModuleId[_files.Keys.Count];
                _files.Keys.CopyTo(keys, 0);
                foreach (ModuleId id in keys) {
                    if (hierarchy.Equals(id.Hierarchy)) {
                        _library.RemoveNode(_files[id]);
                        _files.Remove(id);
                    }
                }
            }
            // Remove the document listeners.
            uint[] docKeys = new uint[_documents.Keys.Count];
            _documents.Keys.CopyTo(docKeys, 0);
            foreach (uint id in docKeys) {
                TextLineEventListener docListener = _documents[id];
                if (hierarchy.Equals(docListener.FileID.Hierarchy)) {
                    _documents.Remove(id);
                    docListener.Dispose();
                }
            }
        }

        public void RegisterLineChangeHandler(uint document,
            TextLineChangeEvent lineChanged, Action<IVsTextLines> onIdle) {
            _documents[document].OnFileChangedImmediate += delegate(object sender, TextLineChange[] changes, int fLast) {
                lineChanged(sender, changes, fLast);
            };
            _documents[document].OnFileChanged += (sender, args) => onIdle(args.TextBuffer);
        }

        #endregion

        #region Library Member Production

        /// <summary>
        /// Overridden in the base class to receive notifications of when a file should
        /// be analyzed for inclusion in the library.  The derived class should queue
        /// the parsing of the file and when it's complete it should call FileParsed
        /// with the provided LibraryTask and an IScopeNode which provides information
        /// about the members of the file.
        /// </summary>
        protected virtual void OnNewFile(LibraryTask task) {
        }

        /// <summary>
        /// Called by derived class when a file has been parsed.  The caller should
        /// provide the LibraryTask received from the OnNewFile call and an IScopeNode
        /// which represents the contents of the library.
        /// 
        /// It is safe to call this method from any thread.
        /// </summary>
        protected void FileParsed(LibraryTask task, IScopeNode scope) {
            try {
                var project = task.ModuleID.Hierarchy.GetProject().GetCommonProject();

                HierarchyNode fileNode = fileNode = project.NodeFromItemId(task.ModuleID.ItemID);
                if (fileNode == null) {
                    return;
                }

                LibraryNode module = CreateFileLibraryNode(
                    fileNode,
                    System.IO.Path.GetFileName(task.FileName),
                    task.FileName,
                    LibraryNodeType.PhysicalContainer
                );

                // TODO: Creating the module tree should be done lazily as needed
                // Currently we replace the entire tree and rely upon the libraries
                // update count to invalidate the whole thing.  We could do this
                // finer grained and only update the changed nodes.  But then we
                // need to make sure we're not mutating lists which are handed out.

                CreateModuleTree(module, module, scope, task.FileName + ":", task.ModuleID);

                if (null != task.ModuleID) {
                    LibraryNode previousItem = null;
                    lock (_files) {
                        if (_files.TryGetValue(task.ModuleID, out previousItem)) {
                            _files.Remove(task.ModuleID);
                        }
                    }
                    _library.RemoveNode(previousItem);
                }
                _library.AddNode(module);
                if (null != task.ModuleID) {
                    lock (_files) {
                        _files.Add(task.ModuleID, module);
                    }
                }
            } catch (COMException) {
                // we're shutting down and can't get the project
            }
        }

        private void CreateModuleTree(LibraryNode root, LibraryNode current, IScopeNode scope, string namePrefix, ModuleId moduleId) {
            if ((null == root) || (null == scope) || (null == scope.NestedScopes)) {
                return;
            }
            foreach (IScopeNode subItem in scope.NestedScopes) {                
                LibraryNode newNode = CreateLibraryNode(subItem, namePrefix, moduleId.Hierarchy, moduleId.ItemID);
                string newNamePrefix = namePrefix;

                // The classes are always added to the root node, the functions to the
                // current node.
                
                if ((newNode.NodeType & LibraryNodeType.Classes) != LibraryNodeType.None) {
                    // Classes are always added to the root.
                    root.AddNode(newNode);
                    newNamePrefix = namePrefix + newNode.Name + ".";
                } else {
                    current.AddNode(newNode);
                }

                // Now use recursion to get the other types.
                CreateModuleTree(root, newNode, subItem, newNamePrefix, moduleId);
            }
        }
        #endregion
        
        #region Hierarchy Events

        private void OnNewFile(object sender, HierarchyEventArgs args) {
            IVsHierarchy hierarchy = sender as IVsHierarchy;
            if (null == hierarchy || IsNonMemberItem(hierarchy, args.ItemID)) {
                return;
            }

            ITextBuffer buffer = null;
            if (null != args.TextBuffer) {
                buffer = _adapterFactory.GetDocumentBuffer(args.TextBuffer);
            }

            var id = new ModuleId(hierarchy, args.ItemID);
            OnNewFile(new LibraryTask(args.CanonicalName, buffer, new ModuleId(hierarchy, args.ItemID)));
        }

        /// <summary>
        /// Handles the delete event, checking to see if this is a project item.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnDeleteFile(object sender, HierarchyEventArgs args) {
            IVsHierarchy hierarchy = sender as IVsHierarchy;
            if (null == hierarchy || IsNonMemberItem(hierarchy, args.ItemID)) {
                return;
            }

            OnDeleteFile(hierarchy, args);
        }

        /// <summary>
        /// Does a delete w/o checking if it's a non-meber item, for handling the
        /// transition from member item to non-member item.
        /// </summary>
        private void OnDeleteFile(IVsHierarchy hierarchy, HierarchyEventArgs args) {
            ModuleId id = new ModuleId(hierarchy, args.ItemID);
            LibraryNode node = null;
            lock (_files) {
                if (_files.TryGetValue(id, out node)) {
                    _files.Remove(id);
                }
            }
            if (null != node) {
                _library.RemoveNode(node);
            }
        }

        private void IsNonMemberItemChanged(object sender, HierarchyEventArgs args) {
            IVsHierarchy hierarchy = sender as IVsHierarchy;
            if (null == hierarchy) {
                return;
            }

            if (!IsNonMemberItem(hierarchy, args.ItemID)) {
                OnNewFile(hierarchy, args);
            } else {
                OnDeleteFile(hierarchy, args);
            }
        }

        /// <summary>
        /// Checks whether this hierarchy item is a project member (on disk items from show all
        /// files aren't considered project members).
        /// </summary>
        protected bool IsNonMemberItem(IVsHierarchy hierarchy, uint itemId) {
            object val;
            int hr = hierarchy.GetProperty(itemId, (int)__VSHPROPID.VSHPROPID_IsNonMemberItem, out val);
            return ErrorHandler.Succeeded(hr) && (bool)val;
        }

        #endregion

        #region IVsRunningDocTableEvents Members

        public int OnAfterAttributeChange(uint docCookie, uint grfAttribs) {
            if ((grfAttribs & (uint)(__VSRDTATTRIB.RDTA_MkDocument)) == (uint)__VSRDTATTRIB.RDTA_MkDocument) {
                IVsRunningDocumentTable rdt = GetPackageService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable;
                if (rdt != null) {
                    uint flags, readLocks, editLocks, itemid;
                    IVsHierarchy hier;
                    IntPtr docData = IntPtr.Zero;
                    string moniker;
                    int hr;
                    try {
                        hr = rdt.GetDocumentInfo(docCookie, out flags, out readLocks, out editLocks, out moniker, out hier, out itemid, out docData);
                        TextLineEventListener listner;
                        if (_documents.TryGetValue(docCookie, out listner)) {
                            listner.FileName = moniker;
                        }
                    } finally {
                        if (IntPtr.Zero != docData) {
                            Marshal.Release(docData);
                        }
                    }
                }
            }
            return VSConstants.S_OK;
        }

        public int OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame) {
            return VSConstants.S_OK;
        }

        public int OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) {
            return VSConstants.S_OK;
        }

        public int OnAfterSave(uint docCookie) {
            return VSConstants.S_OK;
        }

        public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame) {
            // Check if this document is in the list of the documents.
            if (_documents.ContainsKey(docCookie)) {
                return VSConstants.S_OK;
            }
            // Get the information about this document from the RDT.
            IVsRunningDocumentTable rdt = GetPackageService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable;
            if (null != rdt) {
                // Note that here we don't want to throw in case of error.
                uint flags;
                uint readLocks;
                uint writeLoks;
                string documentMoniker;
                IVsHierarchy hierarchy;
                uint itemId;
                IntPtr unkDocData;
                int hr = rdt.GetDocumentInfo(docCookie, out flags, out readLocks, out writeLoks,
                                             out documentMoniker, out hierarchy, out itemId, out unkDocData);
                try {
                    if (Microsoft.VisualStudio.ErrorHandler.Failed(hr) || (IntPtr.Zero == unkDocData)) {
                        return VSConstants.S_OK;
                    }
                    // Check if the herarchy is one of the hierarchies this service is monitoring.
                    if (!_hierarchies.ContainsKey(hierarchy)) {
                        // This hierarchy is not monitored, we can exit now.
                        return VSConstants.S_OK;
                    }

                    // Check the file to see if a listener is required.
                    if (_package.IsRecognizedFile(documentMoniker)) {
                        return VSConstants.S_OK;
                    }                    

                    // Create the module id for this document.
                    ModuleId docId = new ModuleId(hierarchy, itemId);

                    // Try to get the text buffer.
                    IVsTextLines buffer = Marshal.GetObjectForIUnknown(unkDocData) as IVsTextLines;

                    // Create the listener.
                    TextLineEventListener listener = new TextLineEventListener(buffer, documentMoniker, docId);
                    // Set the event handler for the change event. Note that there is no difference
                    // between the AddFile and FileChanged operation, so we can use the same handler.
                    listener.OnFileChanged += new EventHandler<HierarchyEventArgs>(OnNewFile);
                    // Add the listener to the dictionary, so we will not create it anymore.
                    _documents.Add(docCookie, listener);
                } finally {
                    if (IntPtr.Zero != unkDocData) {
                        Marshal.Release(unkDocData);
                    }
                }
            }
            // Always return success.
            return VSConstants.S_OK;
        }

        public int OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) {
            if ((0 != dwEditLocksRemaining) || (0 != dwReadLocksRemaining)) {
                return VSConstants.S_OK;
            }
            TextLineEventListener listener;
            if (!_documents.TryGetValue(docCookie, out listener) || (null == listener)) {
                return VSConstants.S_OK;
            }
            using (listener) {
                _documents.Remove(docCookie);
                // Now make sure that the information about this file are up to date (e.g. it is
                // possible that Class View shows something strange if the file was closed without
                // saving the changes).
                HierarchyEventArgs args = new HierarchyEventArgs(listener.FileID.ItemID, listener.FileName);
                OnNewFile(listener.FileID.Hierarchy, args);
            }
            return VSConstants.S_OK;
        }

        #endregion

        public void OnIdle(IOleComponentManager compMgr) {
            foreach (TextLineEventListener listener in _documents.Values) {
                if (compMgr.FContinueIdle() == 0) {
                    break;
                }

                listener.OnIdle();
            }
        }

        #region IDisposable Members

        public void Dispose() {
            // Dispose all the listeners.
            foreach (HierarchyListener listener in _hierarchies.Values) {
                listener.Dispose();
            }
            _hierarchies.Clear();

            foreach (TextLineEventListener textListener in _documents.Values) {
                textListener.Dispose();
            }
            _documents.Clear();

            // Remove this library from the object manager.
            if (0 != _objectManagerCookie) {
                IVsObjectManager2 mgr = GetPackageService(typeof(SVsObjectManager)) as IVsObjectManager2;
                if (null != mgr) {
                    mgr.UnregisterLibrary(_objectManagerCookie);
                }
                _objectManagerCookie = 0;
            }

            // Unregister this object from the RDT events.
            UnregisterRDTEvents();
        }

        #endregion
    }
}