/* ****************************************************************************
 *
 * 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;
using System.Diagnostics.Contracts;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Threading;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Navigation;
using Microsoft.VisualStudio.Project.Automation;
using MSBuild = Microsoft.Build.Evaluation;
using VsCommands2K = Microsoft.VisualStudio.VSConstants.VSStd2KCmdID;
using VSConstants = Microsoft.VisualStudio.VSConstants;

namespace Microsoft.VisualStudio.Project {

    public enum CommonImageName {
        File = 0,
        Project = 1,
        SearchPathContainer,
        SearchPath,
        MissingSearchPath,
        StartupFile,
        InterpretersContainer = SearchPathContainer,
        Interpreter = SearchPath,
        InterpretersPackage = SearchPath
    }

    public abstract class CommonProjectNode : ProjectNode, IVsProjectSpecificEditorMap2, IVsDeferredSaveProject {
        private CommonProjectPackage/*!*/ _package;
        private Guid _mruPageGuid = new Guid(CommonConstants.AddReferenceMRUPageGuid);
        private VSLangProj.VSProject _vsProject = null;
        private static ImageList _imageList;
        private ProjectDocumentsListenerForStartupFileUpdates _projectDocListenerForStartupFileUpdates;
        private static int _imageOffset;
        private FileSystemWatcher _watcher, _attributesWatcher;
        private int _suppressFileWatcherCount;
        private bool _isRefreshing;
        public bool _boldedStartupItem;
        private bool _showingAllFiles;
        private object _automationObject;
        private PropertyPage _propPage;
        private readonly Dictionary<string, FileSystemEventHandler> _fileChangedHandlers = new Dictionary<string, FileSystemEventHandler>();
        private Queue<FileSystemChange> _fileSystemChanges = new Queue<FileSystemChange>();
        private object _fileSystemChangesLock = new object();
        public UIThreadSynchronizer _uiSync;
        private MSBuild.Project userBuildProject;
        private readonly Dictionary<string, FileSystemWatcher> _symlinkWatchers = new Dictionary<string, FileSystemWatcher>();
        private DiskMerger _currentMerger;
        private int _idleTriggered;

        public CommonProjectNode(CommonProjectPackage/*!*/ package, ImageList/*!*/ imageList) 
            : base(package)
        {
            Contract.Assert(package != null);
            Contract.Assert(imageList != null);

            _package = package;
            CanFileNodesHaveChilds = true;
            OleServiceProvider.AddService(typeof(VSLangProj.VSProject), new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);
            SupportsProjectDesigner = true;
            _imageList = imageList;

            //Store the number of images in ProjectNode so we know the offset of the language icons.
            _imageOffset = ImageHandler.ImageList.Images.Count;
            foreach (Image img in ImageList.Images) {
                ImageHandler.AddImage(img);
            }

            InitializeCATIDs();

            _uiSync = new UIThreadSynchronizer();
            package.OnIdle += OnIdle;
        }

        #region abstract methods

        public abstract Type GetEditorFactoryType();
        public abstract string GetProjectName();

        public virtual CommonFileNode CreateCodeFileNode(ProjectElement item) {
            return new CommonFileNode(this, item);
        }
        public virtual CommonFileNode CreateNonCodeFileNode(ProjectElement item) {
            return new CommonNonCodeFileNode(this, item);
        }
        public abstract string GetFormatList();
        public abstract Type GetLibraryManagerType();
        public abstract Type GetProjectFactoryType();

        #endregion

        #region Properties

        public new CommonProjectPackage/*!*/ Package {
            get { return _package; }
        }

        public static int ImageOffset {
            get { return _imageOffset; }
        }

        /// <summary>
        /// Get the VSProject corresponding to this project
        /// </summary>
        public VSLangProj.VSProject VSProject {
            get {
                if (_vsProject == null)
                    _vsProject = new OAVSProject(this);
                return _vsProject;
            }
        }

        private IVsHierarchy InteropSafeHierarchy {
            get {
                IntPtr unknownPtr = VsUtilities.QueryInterfaceIUnknown(this);
                if (IntPtr.Zero == unknownPtr) {
                    return null;
                }
                IVsHierarchy hier = Marshal.GetObjectForIUnknown(unknownPtr) as IVsHierarchy;
                return hier;
            }
        }

        /// <summary>
        /// Indicates whether the project is currently is busy refreshing its hierarchy.
        /// </summary>
        public bool IsRefreshing {
            get { return _isRefreshing; }
            set { _isRefreshing = value; }
        }

        /// <summary>
        /// Language specific project images
        /// </summary>
        public static ImageList ImageList {
            get {
                return _imageList;
            }
            set {
                _imageList = value;
            }
        }

        public PropertyPage PropertyPage
        {
            get { return _propPage; }
            set { _propPage = value; }
        }

        #endregion

        #region overridden properties

        public override bool CanShowAllFiles {
            get {
                return true;
            }
        }

        public override bool IsShowingAllFiles {
            get {
                return _showingAllFiles;
            }
        }

        /// <summary>
        /// Since we appended the language images to the base image list in the constructor,
        /// this should be the offset in the ImageList of the langauge project icon.
        /// </summary>
        public override int ImageIndex {
            get {
                return _imageOffset + (int)CommonImageName.Project;
            }
        }

        public override Guid ProjectGuid {
            get {
                return GetProjectFactoryType().GUID;
            }
        }
        public override string ProjectType {
            get {
                return GetProjectName();
            }
        }
        public override object Object {
            get {
                return null;
            }
        }
        #endregion

        #region overridden methods

        public override object GetAutomationObject() {
            if (_automationObject == null) {
                _automationObject = base.GetAutomationObject();
            }
            return _automationObject;
        }

        public override int QueryStatusOnNode(Guid cmdGroup, uint cmd, IntPtr pCmdText, ref QueryStatusResult result) {
            if (cmdGroup == CommonConstants.Std97CmdGroupGuid) {
                switch ((VSConstants.VSStd97CmdID)cmd) {
                    case VSConstants.VSStd97CmdID.BuildCtx:
                    case VSConstants.VSStd97CmdID.RebuildCtx:
                    case VSConstants.VSStd97CmdID.CleanCtx:
                        result = QueryStatusResult.SUPPORTED | QueryStatusResult.INVISIBLE;
                        return VSConstants.S_OK;
                }
            } else if (cmdGroup == Microsoft.VisualStudio.Project.VsMenus.guidStandardCommandSet2K) {
                switch ((VsCommands2K)cmd) {
                    case VsCommands2K.ECMD_PUBLISHSELECTION:
                        if (pCmdText != IntPtr.Zero && NativeMethods.OLECMDTEXT.GetFlags(pCmdText) == NativeMethods.OLECMDTEXT.OLECMDTEXTF.OLECMDTEXTF_NAME) {
                            NativeMethods.OLECMDTEXT.SetText(pCmdText, "Publish " + this.Caption);
                        }

                        if (IsPublishingEnabled) {
                            result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED;
                        } else {
                            result |= QueryStatusResult.SUPPORTED;
                        }
                        return VSConstants.S_OK;

                    case VsCommands2K.ECMD_PUBLISHSLNCTX:
                        if (IsPublishingEnabled) {
                            result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED;
                        } else {
                            result |= QueryStatusResult.SUPPORTED;
                        }
                        return VSConstants.S_OK;
                    case CommonConstants.OpenFolderInExplorerCmdId:
                        result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED;
                        return VSConstants.S_OK;
                }
            } else if (cmdGroup == SharedCommandGuid) {
                switch ((SharedCommands)cmd) {
                    case SharedCommands.AddExistingFolder:
                        result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED;
                        return VSConstants.S_OK;
            }
            }

            return base.QueryStatusOnNode(cmdGroup, cmd, pCmdText, ref result);
        }

        private bool IsPublishingEnabled {
            get {
                return !String.IsNullOrWhiteSpace(GetProjectProperty(CommonConstants.PublishUrl));
            }
        }

        public override int ExecCommandOnNode(Guid cmdGroup, uint cmd, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) {
            if (cmdGroup == Microsoft.VisualStudio.Project.VsMenus.guidStandardCommandSet2K) {
                switch ((VsCommands2K)cmd) {
                    case VsCommands2K.ECMD_PUBLISHSELECTION:
                    case VsCommands2K.ECMD_PUBLISHSLNCTX:
                        Publish(PublishProjectOptions.Default, true);
                        return VSConstants.S_OK;
                    case CommonConstants.OpenFolderInExplorerCmdId:
                        Process.Start(this.ProjectHome);
                        return VSConstants.S_OK;
                }
            } else if (cmdGroup == SharedCommandGuid) {
                switch ((SharedCommands)cmd) {
                    case SharedCommands.AddExistingFolder:
                        return AddExistingFolderToNode(this);
            }
            }
            return base.ExecCommandOnNode(cmdGroup, cmd, nCmdexecopt, pvaIn, pvaOut);
        }

        public int AddExistingFolderToNode(HierarchyNode parent) {
            var dir = BrowseForFolder(String.Format("Add Existing Folder - {0}",Caption), parent.FullPathToChildren);
            if (dir != null) {
                DropFilesOrFolders(new[] { dir }, parent);
            }
            return VSConstants.S_OK;
        }

        /// <summary>
        /// Publishes the project as configured by the user in the Publish option page.
        /// 
        /// If async is true this function begins the publishing and returns w/o waiting for it to complete.  No errors are reported.
        /// 
        /// If async is false this function waits for the publish to finish and raises a PublishFailedException with an
        /// inner exception indicating the underlying reason for the publishing failure.
        /// 
        /// Returns true if the publish was succeessfully started, false if the project is not configured for publishing
        /// </summary>
        public bool Publish(PublishProjectOptions publishOptions, bool async) {
            string publishUrl = publishOptions.DestinationUrl ?? GetProjectProperty(CommonConstants.PublishUrl);
            bool found = false;
            if (!String.IsNullOrWhiteSpace(publishUrl)) {
                var url = new Url(publishUrl);

                var publishers = CommonPackage.ComponentModel.GetExtensions<IProjectPublisher>();
                foreach (var publisher in publishers) {
                    if (publisher.Schema == url.Uri.Scheme) {
                        var project = new PublishProject(this, publishOptions);
                        Exception failure = null;
                        var frame = new DispatcherFrame();
                        var thread = new System.Threading.Thread(x => {
                            try {
                                publisher.PublishFiles(project, url.Uri);
                                project.Done();
                                frame.Continue = false;
                            } catch (Exception e) {
                                failure = e;
                                project.Failed(e.Message);
                                frame.Continue = false;
                            }
                        });
                        thread.Start();
                        found = true;
                        if (!async) {
                            Dispatcher.PushFrame(frame);
                            if (failure != null) {
                                throw new PublishFailedException(String.Format("Publishing of the project {0} failed", Caption), failure);
                            }
                        }
                        break;
                    }
                }

                if (!found) {
                    var statusBar = (IVsStatusbar)CommonPackage.GetGlobalService(typeof(SVsStatusbar));
                    statusBar.SetText(String.Format("Publish failed: Unknown publish scheme ({0})", url.Uri.Scheme));
                }
            } else {
                var statusBar = (IVsStatusbar)CommonPackage.GetGlobalService(typeof(SVsStatusbar));
                statusBar.SetText(String.Format("Project is not configured for publishing in project properties."));
            }
            return found;
        }

        public virtual CommonConfig MakeConfiguration(string activeConfigName, string activePlatformName) {
            return new CommonConfig(this, activeConfigName, activePlatformName);
        }

        /// <summary>
        /// As we don't register files/folders in the project file, removing an item is a noop.
        /// </summary>
        public override int RemoveItem(uint reserved, uint itemId, out int result) {
            result = 1;
            return VSConstants.S_OK;
        }

        public override void BuildAsync(uint vsopts, string config, string platform, IVsOutputWindowPane output, string target, IEnumerable<string> files, Action<MSBuildResult, string> uiThreadCallback) {
            uiThreadCallback(MSBuildResult.Successful, target);
        }

        /// <summary>
        /// Overriding main project loading method to inject our hierarachy of nodes.
        /// </summary>
        protected override void Reload() {
            base.Reload();

            OnProjectPropertyChanged += CommonProjectNode_OnProjectPropertyChanged;

            // track file creation/deletes and update our glyphs when files start/stop existing for files in the project.
            if (_watcher != null) {
                _watcher.EnableRaisingEvents = false;
                _watcher.Dispose();
            }

            string userProjectFilename = FileName + ".user";
            if (File.Exists(userProjectFilename)) {
                userBuildProject = BuildProject.ProjectCollection.LoadProject(userProjectFilename);
            }

            bool? showAllFiles = null;
            if (userBuildProject != null) {
                showAllFiles = GetShowAllFilesSetting(userBuildProject.GetPropertyValue(CommonConstants.ProjectView));
            }

            _showingAllFiles = showAllFiles ??
                GetShowAllFilesSetting(BuildProject.GetPropertyValue(CommonConstants.ProjectView)) ??
                false;

            _watcher = CreateFileSystemWatcher(ProjectHome);
            _attributesWatcher = CreateAttributesWatcher(ProjectHome);

            // add everything that's on disk that we don't have in the project
            MergeDiskNodes(this, ProjectHome);

            this.SetProjectFileDirty(false);
        }

        private FileSystemWatcher CreateFileSystemWatcher(string dir) {
            var watcher = new FileSystemWatcher(dir);
            watcher.IncludeSubdirectories = true;
            watcher.Created += new FileSystemEventHandler(FileExistanceChanged);
            watcher.Deleted += new FileSystemEventHandler(FileExistanceChanged);
            watcher.Renamed += new RenamedEventHandler(FileNameChanged);
            watcher.Changed += FileContentsChanged;
            watcher.Error += WatcherError;
            watcher.EnableRaisingEvents = true;
            watcher.InternalBufferSize = 1024 * 4;  // 4k is minimum buffer size
            return watcher;
        }

        private FileSystemWatcher CreateAttributesWatcher(string dir) {
            var watcher = new FileSystemWatcher(dir);
            watcher.NotifyFilter = NotifyFilters.Attributes;
            watcher.Changed += FileAttributesChanged;
            watcher.Error += WatcherError;
            watcher.EnableRaisingEvents = true;
            watcher.InternalBufferSize = 1024 * 4;  // 4k is minimum buffer size
            return watcher;
        }

        /// <summary>
        /// When the file system watcher buffer overflows we need to scan the entire 
        /// directory for changes.
        /// </summary>
        private void WatcherError(object sender, ErrorEventArgs e) {
            lock (_fileSystemChanges) {
                _fileSystemChanges.Clear(); // none of the other changes matter now, we'll rescan the world
                _currentMerger = null;  // abort any current merge now that we have a new one
                _fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.All, null));
                TriggerIdle();
            }
        }

        protected override void SaveMSBuildProjectFileAs(string newFileName) {
            base.SaveMSBuildProjectFileAs(newFileName);

            if (userBuildProject != null) {
                userBuildProject.Save(FileName + ".user");
            }
        }

        protected override void SaveMSBuildProjectFile(string filename) {
            base.SaveMSBuildProjectFile(filename);

            if (userBuildProject != null) {
                userBuildProject.Save(filename + ".user");
            }
        }

        protected override void Dispose(bool disposing) {
            base.Dispose(disposing);
            if (this.userBuildProject != null) {
                userBuildProject.ProjectCollection.UnloadProject(userBuildProject);
            }
            _package.OnIdle -= OnIdle;
        }

        public override int ShowAllFiles() {
            if (!QueryEditProjectFile(false)) {
                return VSConstants.E_FAIL;
            }

            if (_showingAllFiles) {
                UpdateShowAllFiles(this, enabled: false);
            } else {
                UpdateShowAllFiles(this, enabled: true);
                ExpandItem(EXPANDFLAGS.EXPF_ExpandFolder);
            }

            _showingAllFiles = !_showingAllFiles;

            string newPropValue = _showingAllFiles ? CommonConstants.ShowAllFiles : CommonConstants.ProjectFiles;

            var projProperty = BuildProject.GetProperty(CommonConstants.ProjectView);
            if (projProperty != null && 
                !projProperty.IsImported && 
                !String.IsNullOrWhiteSpace(projProperty.EvaluatedValue)) {
                // setting is persisted in main project file, update it there.
                BuildProject.SetProperty(CommonConstants.ProjectView, newPropValue);
            } else {
                // save setting in user project file
                if (userBuildProject == null) {
                    // user project file doesn't exist yet, create it.
                    userBuildProject = new MSBuild.Project(BuildProject.ProjectCollection);
                    userBuildProject.FullPath = FileName + ".user";
                }
                userBuildProject.SetProperty(CommonConstants.ProjectView, newPropValue);
                
            }
            SetProjectFileDirty(true);

            // update project state
            return VSConstants.S_OK;
        }

        private void UpdateShowAllFiles(HierarchyNode node, bool enabled) {
            for (var curNode = node.FirstChild; curNode != null; curNode = curNode.NextSibling) {
                UpdateShowAllFiles(curNode, enabled);

                var allFiles = curNode.ItemNode as AllFilesProjectElement;
                if (allFiles != null) {
                    curNode.IsVisible = enabled;
                    OnInvalidateItems(node);
                }
            }
        }

        private static bool? GetShowAllFilesSetting(string showAllFilesValue) {
            bool? showAllFiles = null;
            string showAllFilesSetting = showAllFilesValue.Trim();
            if (String.Equals(showAllFilesSetting, CommonConstants.ProjectFiles)) {
                showAllFiles = false;
            } else if (String.Equals(showAllFilesSetting, CommonConstants.ShowAllFiles)) {
                showAllFiles = true;
            }
            return showAllFiles;
        }

        /// <summary>
        /// Performs merging of the file system state with the current project hierarchy, bringing them
        /// back into sync.
        /// 
        /// The class can be created, and ContinueMerge should be called until it returns false, at which
        /// point the file system has been merged.  
        /// 
        /// You can wait between calls to ContinueMerge to enable not blocking the UI.
        /// 
        /// If there were changes which came in while the DiskMerger was processing then those changes will still need
        /// to be processed after the DiskMerger completes.
        /// </summary>
        class DiskMerger {
            private readonly string _initialDir;
            private readonly Stack<DirState> _remainingDirs = new Stack<DirState>();
            private readonly CommonProjectNode _project;

            public DiskMerger(CommonProjectNode project, HierarchyNode parent, string dir) {
                _project = project;
                _initialDir = dir;
                _remainingDirs.Push(new DirState(dir, parent));
            }

            /// <summary>
            /// Continues processing the merge request, performing a portion of the full merge possibly
            /// returning before the merge has completed.
            /// 
            /// Returns true if the merge needs to continue, or false if the merge has completed.
            /// </summary>
            public bool ContinueMerge() {
                if (_remainingDirs.Count == 0 ||        // all done
                    !Directory.Exists(_initialDir)) {   // directory went away
                    return false;
                }

                var dir = _remainingDirs.Pop();
                Debug.WriteLine(dir.Name);
                if (!Directory.Exists(dir.Name)) {
                    return true;
                }

                HashSet<HierarchyNode> missingChildren = new HashSet<HierarchyNode>(dir.Parent.AllChildren);
                IEnumerable<string> dirs;
                try {
                    dirs = Directory.EnumerateDirectories(dir.Name);
                } catch {
                    // directory was deleted, we don't have access, etc...
                    return true;
                }

                foreach (var curDir in dirs) {
                    if (_project.IsFileHidden(curDir)) {
                        continue;
                    }
                    if (IsFileSymLink(curDir)) {
                        if (IsRecursiveSymLink(_initialDir, curDir)) {
                            // don't add recursive sym links
                            continue;
                        }

                        // track symlinks, we won't get events on the directory
                        _project.CreateSymLinkWatcher(curDir);
                    }

                    var existing = _project.AddAllFilesFolder(dir.Parent, curDir + Path.DirectorySeparatorChar);
                    missingChildren.Remove(existing);
                    _remainingDirs.Push(new DirState(curDir, existing));
                }

                IEnumerable<string> files;
                try {
                    files = Directory.EnumerateFiles(dir.Name);
                } catch {
                    // directory was deleted, we don't have access, etc...
                    return true;
                }

                foreach (var file in files) {
                    if (!Directory.Exists(dir.Name)) {
                        // file went away
                        break;
                    }
                    if (_project.IsFileHidden(file)) {
                        continue;
                    }
                    missingChildren.Remove(_project.AddAllFilesFile(dir.Parent, file));
                }

                // remove the excluded children which are no longer there
                foreach (var child in missingChildren) {
                    if (child.ItemNode.IsExcluded) {
                        _project.RemoveSubTree(child);
                    }
                }

                return true;
            }

            class DirState {
                public readonly string Name;
                public readonly HierarchyNode Parent;

                public DirState(string name, HierarchyNode parent) {
                    Name = name;
                    Parent = parent;
                }
            }
        }

        private void MergeDiskNodes(HierarchyNode curParent, string dir) {
            var merger = new DiskMerger(this, curParent, dir);
            while (merger.ContinueMerge()) {
            }
        }

        private void RemoveSubTree(HierarchyNode node) {
            foreach (var child in node.AllChildren) {
                RemoveSubTree(child);
            }
            node.Parent.RemoveChild(node);
            _diskNodes.Remove(node.Url);
        }

        private static string GetFinalPathName(string dir) {
            using (var dirHandle = NativeMethods.CreateFile(
                dir,
                NativeMethods.FileDesiredAccess.FILE_LIST_DIRECTORY,
                NativeMethods.FileShareFlags.FILE_SHARE_DELETE |
                    NativeMethods.FileShareFlags.FILE_SHARE_READ |
                    NativeMethods.FileShareFlags.FILE_SHARE_WRITE,
                IntPtr.Zero,
                NativeMethods.FileCreationDisposition.OPEN_EXISTING,
                NativeMethods.FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS,
                IntPtr.Zero
            )) {
                if (!dirHandle.IsInvalid) {
                    uint pathLen = NativeMethods.MAX_PATH + 1;
                    uint res;
                    StringBuilder filePathBuilder;
                    for (; ; ) {
                        filePathBuilder = new StringBuilder(checked((int)pathLen));
                        res = NativeMethods.GetFinalPathNameByHandle(
                            dirHandle,
                            filePathBuilder,
                            pathLen,
                            0
                        );
                        if (res != 0 && res < pathLen) {
                            // we had enough space, and got the filename.
                            break;
                        }
                    }

                    if (res != 0) {
                        Debug.Assert(filePathBuilder.ToString().StartsWith("\\\\?\\"));
                        return filePathBuilder.ToString().Substring(4);
                    }
                }
            }
            return dir;
        }

        private static bool IsRecursiveSymLink(string parentDir, string childDir) {
            if (IsFileSymLink(parentDir)) {
                // figure out the real parent dir so the check below works w/ multiple
                // symlinks pointing at each other
                parentDir = GetFinalPathName(parentDir);
            }

            string finalPath = GetFinalPathName(childDir);
            // check and see if we're recursing infinitely...
            if (CommonUtils.IsSubpathOf(finalPath, parentDir)) {
                // skip this file
                return true;
            }
            return false;
        }

        private static bool IsFileSymLink(string path) {
            try {
                return (File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0;
            } catch (UnauthorizedAccessException) {
                return false;
            } catch (DirectoryNotFoundException) {
                return false;
            } catch (FileNotFoundException) {
                return false;
            }
        }

        private bool IsFileHidden(string path) {
            if (String.Equals(path, FileName, StringComparison.OrdinalIgnoreCase) ||
                String.Equals(path, FileName + ".user", StringComparison.OrdinalIgnoreCase)) {
                return true;
            }

            if(!File.Exists(path) && !Directory.Exists(path)) {
                // if the file has disappeared avoid the exception...
                return false;
            }

            try {
                return (File.GetAttributes(path) & (FileAttributes.Hidden | FileAttributes.System)) != 0;
            } catch (UnauthorizedAccessException) {
                return false;
            } catch (DirectoryNotFoundException) {
                return false;
            } catch (FileNotFoundException) {
                return false;
            }
        }

        /// <summary>
        /// Adds a file which is displayed when Show All Files is enabled
        /// </summary>
        private HierarchyNode AddAllFilesFile(HierarchyNode curParent, string file) {
            var existing = FindNodeByFullPath(file);
            if (existing == null) {
                var newFile = CreateFileNode(new AllFilesProjectElement(file, GetItemType(file), this));
                AddAllFilesNode(curParent, newFile);
                return newFile;
            }
            return existing;
        }

        /// <summary>
        /// Adds a folder which is displayed when Show All files is enabled
        /// </summary>
        private HierarchyNode AddAllFilesFolder(HierarchyNode curParent, string curDir) {
            var folderNode = FindNodeByFullPath(curDir);
            if (folderNode == null) {
                folderNode = CreateFolderNode(new AllFilesProjectElement(curDir, "Folder", this));
                AddAllFilesNode(curParent, folderNode);

                // Solution Explorer will expand the parent when an item is
                // added, which we don't want
                folderNode.ExpandItem(EXPANDFLAGS.EXPF_CollapseFolder);
            }
            return folderNode;
        }

        /// <summary>
        /// Initializes and adds a file or folder visible only when Show All files is enabled
        /// </summary>
        private void AddAllFilesNode(HierarchyNode parent, HierarchyNode newNode) {
            newNode.IsVisible = IsShowingAllFiles;
            newNode.ID = ItemIdMap.Add(newNode);
            parent.AddChild(newNode);
        }

        private void FileContentsChanged(object sender, FileSystemEventArgs e) {
            if (IsClosed) {
                return;
            }

            FileSystemEventHandler handler;
            if (_fileChangedHandlers.TryGetValue(e.FullPath, out handler)) {
                handler(sender, e);
            }
        }

        private void FileAttributesChanged(object sender, FileSystemEventArgs e) {
            lock (_fileSystemChanges) {
                if (NoPendingFileSystemRescan()) {
                    _fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.Changed, e.FullPath));
                    TriggerIdle();
                }
            }
        }

        private bool NoPendingFileSystemRescan() {
            return _fileSystemChanges.Count == 0 || _fileSystemChanges.Peek()._type != WatcherChangeTypes.All;
        }

        public void RegisterFileChangeNotification(FileNode node, FileSystemEventHandler handler) {
            _fileChangedHandlers[node.Url] = handler;
        }

        public void UnregisterFileChangeNotification(FileNode node) {
            _fileChangedHandlers.Remove(node.Url);
        }

        protected override ReferenceContainerNode CreateReferenceContainerNode() {
            return new CommonReferenceContainerNode(this);
        }

        private void FileNameChanged(object sender, RenamedEventArgs e) {
            if (IsClosed) {
                return;
            }

            lock (_fileSystemChanges) {
                // we just generate a delete and creation here - we're just updating the hierarchy
                // either changing icons or updating the non-project elements, so we don't need to
                // generate rename events or anything like that.  This saves us from having to 
                // handle updating the hierarchy in a special way for renames.
                if (NoPendingFileSystemRescan()) {
                    _fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.Deleted, e.OldFullPath, true));
                    _fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.Created, e.FullPath, true));
                    TriggerIdle();
                }
            }
        }

        private void FileExistanceChanged(object sender, FileSystemEventArgs e) {
            if (IsClosed) {
                return;
            }

            lock (_fileSystemChanges) {
                if (NoPendingFileSystemRescan()) {
                    _fileSystemChanges.Enqueue(new FileSystemChange(this, e.ChangeType, e.FullPath));
                    TriggerIdle();
                }
            }
        }

        /// <summary>
        /// If VS is already idle, we won't keep getting idle events, so we need to post a
        /// new event to the queue to flip away from idle and back again.
        /// </summary>
        private void TriggerIdle() {
            if (Interlocked.CompareExchange(ref _idleTriggered, 1, 0) == 0) {
                _uiSync.BeginInvoke(Nop, Type.EmptyTypes);
            }
        }

        private static readonly Action Nop = () => { };

        public void CreateSymLinkWatcher(string curDir) {
            if (!CommonUtils.HasEndSeparator(curDir)) {
                curDir = curDir + Path.DirectorySeparatorChar;
            }
            _symlinkWatchers[curDir] = CreateFileSystemWatcher(curDir);
        }

        public bool TryDeactivateSymLinkWatcher(HierarchyNode child) {
            FileSystemWatcher watcher;
            if (_symlinkWatchers.TryGetValue(child.Url, out watcher)) {
                _symlinkWatchers.Remove(child.Url);
                watcher.EnableRaisingEvents = false;
                watcher.Dispose();
                return true;
            }
            return false;
        }

        private void OnIdle(object sender, ComponentManagerEventArgs e) {
            Interlocked.Exchange(ref _idleTriggered, 0);
            do {
                using (new DebugTimer("ProcessFileChanges while Idle", 100)) {
                    if (IsClosed) {
                        return;
                    }

                    DiskMerger merger;
                    FileSystemChange change = null;
                    lock (_fileSystemChanges) {
                        merger = _currentMerger;
                        if (merger == null) {
                            if (_fileSystemChanges.Count == 0) {
                                break;
                            }

                            change = _fileSystemChanges.Dequeue();
                        }
                    }

                    if (merger != null) {
                        // we have more file merges to process, do this
                        // before reflecting any other pending updates...
                        if (!merger.ContinueMerge()) {
                            _currentMerger = null;
                        }
                        continue;
                    }
#if DEBUG
                    try {
#endif
                        if (change._type == WatcherChangeTypes.All) {
                            _currentMerger = new DiskMerger(this, this, ProjectHome);
                            continue;
                        } else {
                            change.ProcessChange();
                        }
#if DEBUG
                    } catch (Exception ex) {
                        Debug.Fail("Unexpected exception while processing change: {0}", ex.ToString());
                        throw;
                    }
#endif
                }
            } while (e.ComponentManager.FContinueIdle() != 0);
        }

        /// <summary>
        /// Represents an individual change to the file system.  We process these in bulk on the
        /// UI thread.
        /// </summary>
        class FileSystemChange {
            private readonly CommonProjectNode _project;
            public readonly WatcherChangeTypes _type;
            private readonly string _path;
            private readonly bool _isRename;

            public FileSystemChange(CommonProjectNode node, WatcherChangeTypes changeType, string path, bool isRename = false) {
                _project = node;
                _type = changeType;
                _path = path;
                _isRename = isRename;
            }

            public override string ToString() {
                return "FileSystemChange: " + _isRename + " " + _type + " " + _path;
            }

            private void RedrawIcon(HierarchyNode node) {
                _project.ReDrawNode(node, UIHierarchyElement.Icon);

                for (var child = node.FirstChild; child != null; child = child.NextSibling) {
                    RedrawIcon(child);
                }
            }

            public void ProcessChange() {
                var child = _project.FindNodeByFullPath(_path);
                if ((_type == WatcherChangeTypes.Deleted || _type == WatcherChangeTypes.Changed) && child == null) {
                    child = _project.FindNodeByFullPath(_path + Path.DirectorySeparatorChar);
                }
                switch (_type) {
                    case WatcherChangeTypes.Deleted:
                        ChildDeleted(child); 
                        break;
                    case WatcherChangeTypes.Created: ChildCreated(child); break;
                    case WatcherChangeTypes.Changed:
                        // we only care about the attributes
                        if (_project.IsFileHidden(_path)) {
                            if (child != null) {
                                // attributes must of changed to hidden, remove the file
                                goto case WatcherChangeTypes.Deleted;
                            }
                        } else {
                            if (child == null) {
                                // attributes must of changed from hidden, add the file
                                goto case WatcherChangeTypes.Created;
                            }
                        }
                        break;
                }
            }

            private void RemoveAllFilesChildren(HierarchyNode parent) {
                bool removed = false;

                for (var current = parent.FirstChild; current != null; current = current.NextSibling) {
                    // remove our children first
                    RemoveAllFilesChildren(current);

                    _project.TryDeactivateSymLinkWatcher(current);

                    // then remove us if we're an all files node
                    if (current.ItemNode is AllFilesProjectElement) {
                        parent.RemoveChild(current);
                        _project.OnItemDeleted(current);
                        removed = true;
                    }
                }

                if (removed) {
                    _project.OnInvalidateItems(parent);
                }
            }

            private void ChildDeleted(HierarchyNode child) {
                if (child != null) {
                    _project.TryDeactivateSymLinkWatcher(child);

                    if (child.ItemNode.IsExcluded) {
                        RemoveAllFilesChildren(child);

                        // deleting a show all files item, remove the node.
                        child.Parent.RemoveChild(child);
                        _project.OnItemDeleted(child);
                    } else {
                        Debug.Assert(!child.IsNonMemberItem);
                        // deleting an item in the project, fix the icon, also
                        // fix the icon of all children which we may have not
                        // received delete notifications for
                        RedrawIcon(child);
                    }
                }
            }            

            private void ChildCreated(HierarchyNode child) {
                if (child != null) {
                    // creating an item which was in the project, fix the icon.
                    _project.ReDrawNode(child, UIHierarchyElement.Icon);
                } else {
                    if (_project.IsFileHidden(_path)) {
                        // don't add hidden files/folders
                        return;
                    }

                    // creating a new item, need to create the on all files node
                    string parentDir = Path.GetDirectoryName(CommonUtils.TrimEndSeparator(_path)) + Path.DirectorySeparatorChar;
                    HierarchyNode parent;
                    if (CommonUtils.IsSamePath(parentDir, _project.ProjectHome)) {
                        parent = _project;
                    } else {
                        parent = _project.FindNodeByFullPath(parentDir);
                    }

                    if (parent == null) {
                        // we've hit an error while adding too many files, the file system watcher
                        // couldn't keep up.  That's alright, we'll merge the files in correctly 
                        // in a little while...
                        return;
                    }

                    IVsUIHierarchyWindow uiHierarchy = UIHierarchyUtilities.GetUIHierarchyWindow(_project.Site, HierarchyNode.SolutionExplorer);
                    uint curState;
                    uiHierarchy.GetItemState(_project.GetOuterInterface<IVsUIHierarchy>(),
                        parent.ID,
                        (uint)__VSHIERARCHYITEMSTATE.HIS_Expanded,
                        out curState
                    );

                    if (Directory.Exists(_path)) {
                        if (IsFileSymLink(_path)) {
                            if (IsRecursiveSymLink(parentDir, _path)) {
                                // don't add recusrive sym link directory
                                return;
                            }

                            // otherwise we're going to need a new file system watcher
                            _project.CreateSymLinkWatcher(_path);
                        }

                        var folderNode = _project.AddAllFilesFolder(parent, _path + Path.DirectorySeparatorChar);
                        // we may have just moved a directory from another location (e.g. drag and
                        // and drop in explorer), in which case we also need to process the items
                        // which are in the folder that we won't receive create notifications for.

                        if (_isRename) {
                            // First, make sure we don't have any children
                            RemoveAllFilesChildren(folderNode);

                            // then add the folder nodes
                            _project.MergeDiskNodes(folderNode, _path);
                        }

                        _project.OnInvalidateItems(folderNode);
                    } else {
                        _project.AddAllFilesFile(parent, _path);
                    }

                    if ((curState & (uint)__VSHIERARCHYITEMSTATE.HIS_Expanded) == 0) {
                        // Solution Explorer will expand the parent when an item is
                        // added, which we don't want, so we check it's state before
                        // adding, and then collapse the folder if it was expanded.
                        parent.ExpandItem(EXPANDFLAGS.EXPF_CollapseFolder);
                    }
                }
            }
        }


        public override int GetGuidProperty(int propid, out Guid guid) {
            if ((__VSHPROPID)propid == __VSHPROPID.VSHPROPID_PreferredLanguageSID) {
                guid = new Guid("{EFB9A1D6-EA71-4F38-9BA7-368C33FCE8DC}");// GetLanguageServiceType().GUID;
            } else {
                return base.GetGuidProperty(propid, out guid);
            }
            return VSConstants.S_OK;
        }

        protected override bool IsItemTypeFileType(string type) {
            if (!base.IsItemTypeFileType(type)) {
                if (String.Compare(type, "Page", StringComparison.OrdinalIgnoreCase) == 0
                || String.Compare(type, "ApplicationDefinition", StringComparison.OrdinalIgnoreCase) == 0
                || String.Compare(type, "Resource", StringComparison.OrdinalIgnoreCase) == 0) {
                    return true;
                } else {
                    return false;
                }
            } else {
                //This is a well known item node type, so return true.
                return true;
            }
        }

        protected override NodeProperties CreatePropertiesObject() {
            return new CommonProjectNodeProperties(this);
        }

        public override int SetSite(Microsoft.VisualStudio.OLE.Interop.IServiceProvider site) {
            base.SetSite(site);

            //Initialize a new object to track project document changes so that we can update the StartupFile Property accordingly
            _projectDocListenerForStartupFileUpdates = new ProjectDocumentsListenerForStartupFileUpdates((ServiceProvider)Site, this);
            _projectDocListenerForStartupFileUpdates.Init();

            return VSConstants.S_OK;
        }

        public override void Close() {
            if (null != _projectDocListenerForStartupFileUpdates) {
                _projectDocListenerForStartupFileUpdates.Dispose();
                _projectDocListenerForStartupFileUpdates = null;
            }

            if (GetLibraryManagerType() != null)
            {
                LibraryManager libraryManager = ((IServiceContainer)Package).GetService(GetLibraryManagerType()) as LibraryManager;
                if (null != libraryManager)
                {
                    libraryManager.UnregisterHierarchy(InteropSafeHierarchy);
                }
            }

            if (_watcher != null)
            {
                _watcher.EnableRaisingEvents = false;
                _watcher.Dispose();
                _watcher = null;
            }

            if (_attributesWatcher != null)
            {
                _attributesWatcher.EnableRaisingEvents = false;
                _attributesWatcher.Dispose();
                _attributesWatcher = null;
            }

            base.Close();
        }

        public override void Load(string filename, string location, string name, uint flags, ref Guid iidProject, out int canceled) {
            base.Load(filename, location, name, flags, ref iidProject, out canceled);
            LibraryManager libraryManager = Site.GetService(GetLibraryManagerType()) as LibraryManager;
            if (null != libraryManager) {
                libraryManager.RegisterHierarchy(InteropSafeHierarchy);
            }


            //If this is a WPFFlavor-ed project, then add a project-level DesignerContext service to provide
            //event handler generation (EventBindingProvider) for the XAML designer.
            //this.OleServiceProvider.AddService(typeof(DesignerContext), new OleServiceProvider.ServiceCreatorCallback(this.CreateServices), false);
        }

        public void BoldStartupItem(HierarchyNode startupItem) {
            if (!_boldedStartupItem) {
                if (BoldItem(startupItem, true)) {
                    _boldedStartupItem = true;
                }
            }
        }

        public bool BoldItem(HierarchyNode node, bool isBold) {
            IVsUIHierarchyWindow2 windows = GetUIHierarchyWindow(
                Site as IServiceProvider,
                new Guid(ToolWindowGuids80.SolutionExplorer)) as IVsUIHierarchyWindow2;

            if (ErrorHandler.Succeeded(windows.SetItemAttribute(
                this.GetOuterInterface<IVsUIHierarchy>(),
                node.ID,
                (uint)__VSHIERITEMATTRIBUTE.VSHIERITEMATTRIBUTE_Bold,
                isBold
            ))) {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Overriding to provide project general property page
        /// </summary>
        /// <returns></returns>
        protected override Guid[] GetConfigurationIndependentPropertyPages() {
            return new Guid[0];
        }

        /// <summary>
        /// Create a file node based on an msbuild item.
        /// </summary>
        /// <param name="item">The msbuild item to be analyzed</param>        
        public override FileNode CreateFileNode(ProjectElement item) {
            VsUtilities.ArgumentNotNull("item", item);

            CommonFileNode newNode;
            if (IsCodeFile(item.GetFullPathForElement())) {
                newNode = CreateCodeFileNode(item);
            } else {
                newNode = CreateNonCodeFileNode(item);
            }

            string link = item.GetMetadata(ProjectFileConstants.Link);
            if (!String.IsNullOrWhiteSpace(link) ||
                !CommonUtils.IsSubpathOf(ProjectHome, item.GetFullPathForElement())) {
                newNode.SetIsLinkFile(true);
            }

            string include = item.GetMetadata(ProjectFileConstants.Include);

            newNode.OleServiceProvider.AddService(typeof(EnvDTE.Project),
                new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);
            newNode.OleServiceProvider.AddService(typeof(EnvDTE.ProjectItem), newNode.ServiceCreator, false);

            /*if (!string.IsNullOrEmpty(include) && Path.GetExtension(include).Equals(".xaml", StringComparison.OrdinalIgnoreCase)) {
                //Create a DesignerContext for the XAML designer for this file
                newNode.OleServiceProvider.AddService(typeof(DesignerContext), newNode.ServiceCreator, false);
            }*/

            newNode.OleServiceProvider.AddService(typeof(VSLangProj.VSProject),
                new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);
            return newNode;
        }

        /// <summary>
        /// Create a file node based on absolute file name.
        /// </summary>
        public override FileNode CreateFileNode(string absFileName) {
            // Avoid adding files to the project multiple times.  Ultimately           
            // we should not use project items and instead should have virtual items.       

            string path = CommonUtils.GetRelativeFilePath(ProjectHome, absFileName);
            var prjItem = BuildProject.AddItem(GetItemType(path), path)[0];

            return CreateFileNode(new MsBuildProjectElement(this, prjItem));
        }

        public override FolderNode CreateFolderNode(ProjectElement element)
        {
            return new CommonFolderNode(this, element);
        }

        public string GetItemType(string filename) {
            if (IsCodeFile(filename)) {
                return "Compile";
            } else {
                return "Content";
            }
        }

        public ProjectElement MakeProjectElement(string type, string path) {
            var item = BuildProject.AddItem(type, path)[0];
            return new MsBuildProjectElement(this, item);
        }

        public override int IsDirty(out int isDirty) {
            isDirty = 0;
            if (IsProjectFileDirty) {
                isDirty = 1;
                return VSConstants.S_OK;
            }

            isDirty = IsFlavorDirty();
            return VSConstants.S_OK;
        }

        protected override void AddNewFileNodeToHierarchy(HierarchyNode parentNode, string fileName) {
            base.AddNewFileNodeToHierarchy(parentNode, fileName);

            SetProjectFileDirty(true);
        }

        public override DependentFileNode CreateDependentFileNode(MsBuildProjectElement item) {
            DependentFileNode node = base.CreateDependentFileNode(item);
            if (null != node) {
                string include = item.GetMetadata(ProjectFileConstants.Include);
                if (IsCodeFile(include)) {
                    node.OleServiceProvider.AddService(
                        typeof(SVSMDCodeDomProvider), new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);
                }
            }

            return node;
        }

        /// <summary>
        /// Creates the format list for the open file dialog
        /// </summary>
        /// <param name="formatlist">The formatlist to return</param>
        /// <returns>Success</returns>
        public override int GetFormatList(out string formatlist) {
            formatlist = GetFormatList();
            formatlist = formatlist.Replace('|', '\0') + '\0';

            return VSConstants.S_OK;
        }

        protected override ConfigProvider CreateConfigProvider() {
            return new CommonConfigProvider(this);
        }
        #endregion

        #region Methods

        /// <summary>
        /// This method retrieves an instance of a service that 
        /// allows to start a project or a file with or without debugging.
        /// </summary>
        public abstract IProjectLauncher/*!*/ GetLauncher();

        /// <summary>
        /// Returns resolved value of the current working directory property.
        /// </summary>
        public string GetWorkingDirectory() {
            string workDir = GetProjectProperty(CommonConstants.WorkingDirectory, true);

            return CommonUtils.GetAbsoluteDirectoryPath(ProjectHome, workDir);
        }

        /// <summary>
        /// Returns resolved value of the startup file property.
        /// </summary>
        public string GetStartupFile() {
            string startupFile = GetProjectProperty(CommonConstants.StartupFile, true);

            if (string.IsNullOrEmpty(startupFile)) {
                //No startup file is assigned
                return null;
            }

            return CommonUtils.GetAbsoluteFilePath(ProjectHome, startupFile);
        }

        /// <summary>
        /// Whenever project property has changed - refresh project hierarachy.
        /// </summary>
        private void CommonProjectNode_OnProjectPropertyChanged(object sender, ProjectPropertyChangedArgs e) {
            switch (e.PropertyName) {
                case CommonConstants.StartupFile:
                    RefreshStartupFile(this,
                        CommonUtils.GetAbsoluteFilePath(ProjectHome, e.OldValue),
                        CommonUtils.GetAbsoluteFilePath(ProjectHome, e.NewValue));
                    break;
            }
        }

        /// <summary>
        /// Same as VsShellUtilities.GetUIHierarchyWindow, but it doesn't contain a useless cast to IVsWindowPane
        /// which fails on Dev10 with the solution explorer window.
        /// </summary>
        private static IVsUIHierarchyWindow GetUIHierarchyWindow(IServiceProvider serviceProvider, Guid guidPersistenceSlot) {
            if (serviceProvider == null) {
                throw new ArgumentException("serviceProvider");
            }
            IVsUIShell service = serviceProvider.GetService(typeof(SVsUIShell)) as IVsUIShell;
            if (service == null) {
                throw new InvalidOperationException();
            }
            object pvar = null;
            IVsWindowFrame ppWindowFrame = null;
            IVsUIHierarchyWindow window = null;
            try {
                ErrorHandler.ThrowOnFailure(service.FindToolWindow(0, ref guidPersistenceSlot, out ppWindowFrame));
                ErrorHandler.ThrowOnFailure(ppWindowFrame.GetProperty(-3001, out pvar));
            } catch (COMException exception) {
                Trace.WriteLine("Exception :" + exception.Message);
            } finally {
                if (pvar != null) {
                    window = (IVsUIHierarchyWindow)pvar;
                }
            }
            return window;
        }

        /// <summary>
        /// Returns first immediate child node (non-recursive) of a given type.
        /// </summary>
        private void RefreshStartupFile(HierarchyNode parent, string oldFile, string newFile) {
            IVsUIHierarchyWindow2 windows = GetUIHierarchyWindow(
                Site,
                new Guid(ToolWindowGuids80.SolutionExplorer)) as IVsUIHierarchyWindow2;

            for (HierarchyNode n = parent.FirstChild; n != null; n = n.NextSibling) {
                // TODO: Distinguish between real Urls and fake ones (eg. "References")
                if (windows != null) {
                    var absUrl = CommonUtils.GetAbsoluteFilePath(parent.ProjectMgr.ProjectHome, n.Url);
                    if (CommonUtils.IsSamePath(oldFile, absUrl)) {
                        windows.SetItemAttribute(
                            this,
                            n.ID,
                            (uint)__VSHIERITEMATTRIBUTE.VSHIERITEMATTRIBUTE_Bold,
                            false
                        );
                        ReDrawNode(n, UIHierarchyElement.Icon);
                    } else if (CommonUtils.IsSamePath(newFile, absUrl)) {
                        windows.SetItemAttribute(
                            this,
                            n.ID,
                            (uint)__VSHIERITEMATTRIBUTE.VSHIERITEMATTRIBUTE_Bold,
                            true
                        );
                        ReDrawNode(n, UIHierarchyElement.Icon);
                    }
                }

                RefreshStartupFile(n, oldFile, newFile);
            }
        }

        /// <summary>
        /// Provide mapping from our browse objects and automation objects to our CATIDs
        /// </summary>
        private void InitializeCATIDs() {
            // The following properties classes are specific to current language so we can use their GUIDs directly
            AddCATIDMapping(typeof(OAProject), typeof(OAProject).GUID);
            // The following is not language specific and as such we need a separate GUID
            AddCATIDMapping(typeof(FolderNodeProperties), new Guid(CommonConstants.FolderNodePropertiesGuid));
            // These ones we use the same as language file nodes since both refer to files
            AddCATIDMapping(typeof(FileNodeProperties), typeof(FileNodeProperties).GUID);
            AddCATIDMapping(typeof(IncludedFileNodeProperties), typeof(IncludedFileNodeProperties).GUID);
            // We could also provide CATIDs for references and the references container node, if we wanted to.
        }

        /// <summary>
        /// Parses SearchPath property into a list of distinct absolute paths, preserving the order.
        /// </summary>
        protected IList<string> ParseSearchPath() {
            var searchPath = GetProjectProperty(CommonConstants.SearchPath, true);
            return ParseSearchPath(searchPath);
        }

        /// <summary>
        /// Parses SearchPath string into a list of distinct absolute paths, preserving the order.
        /// </summary>
        protected IList<string> ParseSearchPath(string searchPath) {
            var result = new List<string>();

            if (!string.IsNullOrEmpty(searchPath)) {
                var seen = new HashSet<string>();
                foreach (var path in searchPath.Split(';')) {
                    if (string.IsNullOrEmpty(path)) {
                        continue;
                    }

                    var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, path);
                    if (seen.Add(absPath)) {
                        result.Add(absPath);
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Saves list of paths back as SearchPath project property.
        /// </summary>
        private void SaveSearchPath(IList<string> value) {
            var valueStr = string.Join(";", value.Select(path => {
                var relPath = CommonUtils.GetRelativeFilePath(ProjectHome, path);
                if (string.IsNullOrEmpty(relPath)) {
                    relPath = ".";
            }
                return relPath;
            }));
            SetProjectProperty(CommonConstants.SearchPath, valueStr);
        }

        /// <summary>
        /// Adds new search path to the SearchPath project property.
        /// </summary>
        public void AddSearchPathEntry(string newpath) {
            VsUtilities.ArgumentNotNull("newpath", newpath);

            IList<string> searchPath = ParseSearchPath();
            var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, newpath);
            if (searchPath.Contains(absPath, StringComparer.OrdinalIgnoreCase)) {
                return;
            }
            searchPath.Add(absPath);
            SaveSearchPath(searchPath);
        }

        /// <summary>
        /// Removes a given path from the SearchPath property.
        /// </summary>
        public void RemoveSearchPathEntry(string path) {
            IList<string> searchPath = ParseSearchPath();
            var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, path);
            if (searchPath.Remove(path)) {
                SaveSearchPath(searchPath);
            }
        }

        /// <summary>
        /// Creates the services exposed by this project.
        /// </summary>
        private object CreateServices(Type serviceType) {
            object service = null;
            if (typeof(VSLangProj.VSProject) == serviceType) {
                service = VSProject;
            } else if (typeof(EnvDTE.Project) == serviceType) {
                service = GetAutomationObject();
            } /*else if (typeof(DesignerContext) == serviceType) {
                service = this.DesignerContext;
            }*/

            return service;
        }

        /// <summary>
        /// Executes Add Search Path menu command.
        /// </summary>        
        public int AddSearchPath() {
            string dirName = BrowseForFolder(
                DynamicProjectSR.GetString(DynamicProjectSR.SelectFolderForSearchPath), 
                ProjectHome);

            if (dirName != null) {
                AddSearchPathEntry(dirName);
            }
            
            return VSConstants.S_OK;
        }

        public string BrowseForFolder(string title, string initialDir) {
            // Get a reference to the UIShell.
            IVsUIShell uiShell = GetService(typeof(SVsUIShell)) as IVsUIShell;
            if (null == uiShell) {
                return null;
            }

            //Create a fill in a structure that defines Browse for folder dialog
            VSBROWSEINFOW[] browseInfo = new VSBROWSEINFOW[1];
            //Dialog title
            browseInfo[0].pwzDlgTitle = title;
            //Initial directory - project directory
            browseInfo[0].pwzInitialDir = initialDir;
            //Parent window
            uiShell.GetDialogOwnerHwnd(out browseInfo[0].hwndOwner);
            //Max path length
            //This is WCHARS not bytes
            browseInfo[0].nMaxDirName = (uint)NativeMethods.MAX_PATH;
            //This struct size
            browseInfo[0].lStructSize = (uint)Marshal.SizeOf(typeof(VSBROWSEINFOW));
            //Memory to write selected directory to.
            //Note: this one allocates unmanaged memory, which must be freed later
            //  Add 1 for the null terminator and double since we are WCHARs not bytes
            IntPtr pDirName = Marshal.AllocCoTaskMem((NativeMethods.MAX_PATH + 1)*2);
            browseInfo[0].pwzDirName = pDirName;
            try {
                //Show the dialog
                int hr = uiShell.GetDirectoryViaBrowseDlg(browseInfo);
                if (hr == VSConstants.OLE_E_PROMPTSAVECANCELLED) {
                    //User cancelled the dialog
                    return null;
                }
                //Check for any failures
                ErrorHandler.ThrowOnFailure(hr);
                //Get selected directory
                return Marshal.PtrToStringAuto(browseInfo[0].pwzDirName);
            } finally {
                //Free allocated unmanaged memory
                if (pDirName != IntPtr.Zero) {
                    Marshal.FreeCoTaskMem(pDirName);
                }
            }
        }

        #endregion

        #region IVsProjectSpecificEditorMap2 Members

        public int GetSpecificEditorProperty(string mkDocument, int propid, out object result) {
            // initialize output params
            result = null;

            //Validate input
            if (string.IsNullOrEmpty(mkDocument))
                throw new ArgumentException("Was null or empty", "mkDocument");

            // Make sure that the document moniker passed to us is part of this project
            // We also don't care if it is not a dynamic language file node
            uint itemid;
            int hr;
            if (ErrorHandler.Failed(hr = ParseCanonicalName(mkDocument, out itemid))) {
                return hr;
            }
            HierarchyNode hierNode = NodeFromItemId(itemid);
            if (hierNode == null || ((hierNode as CommonFileNode) == null))
                return VSConstants.E_NOTIMPL;

            switch (propid) {
                case (int)__VSPSEPROPID.VSPSEPROPID_UseGlobalEditorByDefault:
                    // don't show project default editor, every file supports Python.
                    result = false;
                    return VSConstants.E_FAIL;
                /*case (int)__VSPSEPROPID.VSPSEPROPID_ProjectDefaultEditorName:
                    result = "Python Editor";
                    break;*/
            }

            return VSConstants.S_OK;
        }

        public int GetSpecificEditorType(string mkDocument, out Guid guidEditorType) {
            // Ideally we should at this point initalize a File extension to EditorFactory guid Map e.g.
            // in the registry hive so that more editors can be added without changing this part of the
            // code. Dynamic languages only make usage of one Editor Factory and therefore we will return 
            // that guid
            guidEditorType = GetEditorFactoryType().GUID;
            return VSConstants.S_OK;
        }

        public int GetSpecificLanguageService(string mkDocument, out Guid guidLanguageService) {
            guidLanguageService = Guid.Empty;
            return VSConstants.E_NOTIMPL;
        }

        public int SetSpecificEditorProperty(string mkDocument, int propid, object value) {
            return VSConstants.E_NOTIMPL;
        }

        #endregion

        #region IVsDeferredSaveProject Members

        /// <summary>
        /// Implements deferred save support.  Enabled by unchecking Tools->Options->Solutions and Projects->Save New Projects Created.
        /// 
        /// In this mode we save the project when the user selects Save All.  We need to move all the files in the project
        /// over to the new location.
        /// </summary>
        public virtual int SaveProjectToLocation(string pszProjectFilename) {
            string oldName = Url;
            string basePath = CommonUtils.NormalizeDirectoryPath(Path.GetDirectoryName(this.FileName));
            string newName = Path.GetDirectoryName(pszProjectFilename);

            IVsUIShell shell = this.Site.GetService(typeof(SVsUIShell)) as IVsUIShell;
            IVsSolution vsSolution = (IVsSolution)this.GetService(typeof(SVsSolution));

            int canContinue;
            vsSolution.QueryRenameProject(this, FileName, pszProjectFilename, 0, out canContinue);
            if (canContinue == 0) {
                return VSConstants.OLE_E_PROMPTSAVECANCELLED;
            }

            _watcher.EnableRaisingEvents = false;
            _watcher.Dispose();

            // we don't use RenameProjectFile because it sends the OnAfterRenameProject event too soon
            // and causes VS to think the solution has changed on disk.  We need to send it after all 
            // updates are complete.

            // save the new project to to disk
            SaveMSBuildProjectFileAs(pszProjectFilename);

            if (CommonUtils.IsSameDirectory(ProjectHome, basePath)) {
                // ProjectHome was set by SaveMSBuildProjectFileAs to point to the temporary directory.
                BuildProject.SetProperty(CommonConstants.ProjectHome, ".");

                // save the project again w/ updated file info
                BuildProjectLocationChanged();

                // remove all the children, saving any dirty files, and collecting the list of open files
                MoveFilesForDeferredSave(this, basePath, newName);
            } else {
                // Project referenced external files only, so just update its location without moving
                // files around.
                BuildProjectLocationChanged();
            }

            BuildProject.Save();

            SetProjectFileDirty(false);

            // update VS that we've changed the project
            this.OnPropertyChanged(this, (int)__VSHPROPID.VSHPROPID_Caption, 0);

            // Update solution
            // Note we ignore the errors here because reporting them to the user isn't really helpful.
            // We've already completed all of the work to rename everything here.  If OnAfterNameProject
            // fails for some reason then telling the user it failed is just confusing because all of
            // the work is done.  And if someone wanted to prevent us from renaming the project file they
            // should have responded to QueryRenameProject.  Likewise if we can't refresh the property browser 
            // for any reason then that's not too interesting either - the users project has been saved to 
            // the new location.
            // http://pytools.codeplex.com/workitem/489
            vsSolution.OnAfterRenameProject((IVsProject)this, oldName, pszProjectFilename, 0);

            shell.RefreshPropertyBrowser(0);

            _watcher = CreateFileSystemWatcher(ProjectHome);
            _attributesWatcher = CreateAttributesWatcher(ProjectHome);

            return VSConstants.S_OK;
        }

        private void MoveFilesForDeferredSave(HierarchyNode node, string basePath, string baseNewPath) {
            if (node != null) {
                for (var child = node.FirstChild; child != null; child = child.NextSibling) {
                    bool isOpen, isDirty, isOpenedByUs;
                    uint docCookie;
                    IVsPersistDocData persist;
                    var docMgr = child.GetDocumentManager();
                    if (docMgr != null) {
                        docMgr.GetDocInfo(out isOpen, out isDirty, out isOpenedByUs, out docCookie, out persist);
                        int cancelled;
                        if (isDirty) {
                            child.ProjectMgr.SaveItem(VSSAVEFLAGS.VSSAVE_Save, null, docCookie, IntPtr.Zero, out cancelled);
                        }
                    }

                    IDiskBasedNode diskNode = child as IDiskBasedNode;
                    if (diskNode != null) {
                        diskNode.RenameForDeferredSave(basePath, baseNewPath);
                    }

                    MoveFilesForDeferredSave(child, basePath, baseNewPath);
                }
            }
        }

        #endregion

        public void SuppressFileChangeNotifications() {
            _watcher.EnableRaisingEvents = false;
            _suppressFileWatcherCount++;
        }

        public void RestoreFileChangeNotifications() {
            if (--_suppressFileWatcherCount == 0) {
                _watcher.EnableRaisingEvents = true;
            }
        }

#if DEV11_OR_LATER
        public override object GetProperty(int propId) {            
            CommonFolderNode.BoldStartupOnIcon(propId, this);

            return base.GetProperty(propId);
        }
#else
        public override int SetProperty(int propid, object value) {
            CommonFolderNode.BoldStartupOnExpand(propid, this);

            return base.SetProperty(propid, value);
        }
#endif
    }
}