/* ****************************************************************************
 *
 * 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.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;
using MSBuildExecution = Microsoft.Build.Execution;

namespace Microsoft.VisualStudio.Project
{
    /// <summary>
    /// Allows projects to group outputs according to usage.
    /// </summary>
    [ComVisible(true)]
    public class OutputGroup : IVsOutputGroup2
    {
        private readonly Config _projectCfg;
        private readonly ProjectNode _project;
        private readonly List<Output> _outputs = new List<Output>();
        private readonly string _name;
        private readonly string _targetName;
        private Output _keyOutput;


        /// <summary>
        /// Constructor for IVSOutputGroup2 implementation
        /// </summary>
        /// <param name="outputName">Name of the output group. See VS_OUTPUTGROUP_CNAME_Build in vsshell.idl for the list of standard values</param>
        /// <param name="msBuildTargetName">MSBuild target name</param>
        /// <param name="projectManager">Project that produce this output</param>
        /// <param name="configuration">Configuration that produce this output</param>
        public OutputGroup(string outputName, string msBuildTargetName, ProjectNode projectManager, Config configuration)
        {
            VsUtilities.ArgumentNotNull("outputName", outputName);
            VsUtilities.ArgumentNotNull("msBuildTargetName", msBuildTargetName);
            VsUtilities.ArgumentNotNull("projectManager", projectManager);
            VsUtilities.ArgumentNotNull("configuration", configuration);

            _name = outputName;
            _targetName = msBuildTargetName;
            _project = projectManager;
            _projectCfg = configuration;
        }

        /// <summary>
        /// Get the project configuration object associated with this output group
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Cfg")]
        protected Config ProjectCfg
        {
            get { return _projectCfg; }
        }

        /// <summary>
        /// Get the project object that produces this output group.
        /// </summary>
        public ProjectNode Project
        {
            get { return _project; }
        }

        /// <summary>
        /// Gets the msbuild target name which is assciated to the outputgroup.
        /// ProjectNode defines a static collection of output group names and their associated MsBuild target
        /// </summary>
        protected string TargetName
        {
            get { return _targetName; }
        }

        #region virtual methods

        protected virtual void Refresh()
        {
            IVsCfg cfg;
            ErrorHandler.ThrowOnFailure(_projectCfg.GetCfg(out cfg));

            string displayName;
            ErrorHandler.ThrowOnFailure(cfg.get_DisplayName(out displayName));
            string[] parts = displayName.Split('|');

            string configName = parts[0].Trim();
            string platformName = string.Empty;
            if (parts.Length > 1)
                platformName = parts[1].Trim();

            // Let MSBuild know which configuration we are working with
            _project.SetConfiguration(configName, platformName);

            // Generate dependencies if such a task exist
            const string generateDependencyList = "AllProjectOutputGroups";
            if (_project.BuildProject.Targets.ContainsKey(generateDependencyList))
            {
                //bool succeeded = false;
                //project.BuildTarget(generateDependencyList, out succeeded);
                //Debug.Assert(succeeded, "Failed to build target: " + generateDependencyList);
            }

            // Rebuild the content of our list of output
            string outputType = this._targetName + "Output";
            this._outputs.Clear();

            if (_project.CurrentConfig != null)
            {
                foreach (MSBuildExecution.ProjectItemInstance assembly in _project.CurrentConfig.GetItems(outputType))
                {
                    Output output = new Output(_project, assembly);
                    _outputs.Add(output);

                    // See if it is our key output
                    if (_keyOutput == null ||
                        String.Compare(assembly.GetMetadataValue("IsKeyOutput"), true.ToString(), StringComparison.OrdinalIgnoreCase) == 0)
                    {
                        _keyOutput = output;
                    }
                }
            }

            _project.SetCurrentConfiguration();

            // Now that the group is built we have to check if it is invalidated by a property
            // change on the project.
            _project.OnProjectPropertyChanged += new EventHandler<ProjectPropertyChangedArgs>(OnProjectPropertyChanged);
        }

        public virtual void InvalidateGroup()
        {
            // Set keyOutput to null so that a refresh will be performed the next time
            // a property getter is called.
            if (null != _keyOutput)
            {
                // Once the group is invalidated there is no more reason to listen for events.
                _project.OnProjectPropertyChanged -= new EventHandler<ProjectPropertyChangedArgs>(OnProjectPropertyChanged);
            }
            _keyOutput = null;
        }
        #endregion

        #region event handlers
        private void OnProjectPropertyChanged(object sender, ProjectPropertyChangedArgs args)
        {
            // In theory here we should decide if we have to invalidate the group according with the kind of property
            // that is changed.
            InvalidateGroup();
        }
        #endregion

        #region IVsOutputGroup2 Members

        public virtual int get_CanonicalName(out string pbstrCanonicalName)
        {
            pbstrCanonicalName = this._name;
            return VSConstants.S_OK;
        }

        public virtual int get_DeployDependencies(uint celt, IVsDeployDependency[] rgpdpd, uint[] pcActual)
        {
            return VSConstants.E_NOTIMPL;
        }

        public virtual int get_Description(out string pbstrDescription)
        {
            pbstrDescription = null;

            string description;
            int hr = this.get_CanonicalName(out description);
            if (ErrorHandler.Succeeded(hr))
                pbstrDescription = this.Project.GetOutputGroupDescription(description);
            return hr;
        }

        public virtual int get_DisplayName(out string pbstrDisplayName)
        {
            pbstrDisplayName = null;

            string displayName;
            int hr = this.get_CanonicalName(out displayName);
            if (ErrorHandler.Succeeded(hr))
                pbstrDisplayName = this.Project.GetOutputGroupDisplayName(displayName);
            return hr;
        }

        public virtual int get_KeyOutput(out string pbstrCanonicalName)
        {
            pbstrCanonicalName = null;
            if (_keyOutput == null)
                Refresh();
            if (_keyOutput == null)
            {
                pbstrCanonicalName = String.Empty;
                return VSConstants.S_FALSE;
            }
            return _keyOutput.get_CanonicalName(out pbstrCanonicalName);
        }

        public virtual int get_KeyOutputObject(out IVsOutput2 ppKeyOutput)
        {
            if (_keyOutput == null)
            {
                Refresh();
                if (_keyOutput == null)
                {
                    // horrible hack: we don't really have outputs but the Cider designer insists 
                    // that we have an output so it can figure out our output assembly name.  So we
                    // lie here, and then lie again to give a path in Output.get_Property
                    _keyOutput = new Output(_project, null);
                }
            }
            ppKeyOutput = _keyOutput;
            if (ppKeyOutput == null)
                return VSConstants.S_FALSE;
            return VSConstants.S_OK;
        }

        public virtual int get_Outputs(uint celt, IVsOutput2[] rgpcfg, uint[] pcActual)
        {
            // Ensure that we are refreshed.  This is somewhat of a hack that enables project to
            // project reference scenarios to work.  Normally, output groups are populated as part
            // of build.  However, in the project to project reference case, what ends up happening
            // is that the referencing projects requests the referenced project's output group
            // before a build is done on the referenced project.
            //
            // Furthermore, the project auto toolbox manager requires output groups to be populated
            // on project reopen as well...
            //
            // In the end, this is probably the right thing to do, though -- as it keeps the output
            // groups always up to date.
            Refresh();

            // See if only the caller only wants to know the count
            if (celt == 0 || rgpcfg == null)
            {
                if (pcActual != null && pcActual.Length > 0)
                    pcActual[0] = (uint)_outputs.Count;
                return VSConstants.S_OK;
            }

            // Fill the array with our outputs
            uint count = 0;
            foreach (Output output in _outputs)
            {
                if (rgpcfg.Length > count && celt > count && output != null)
                {
                    rgpcfg[count] = output;
                    ++count;
                }
            }

            if (pcActual != null && pcActual.Length > 0)
                pcActual[0] = count;

            // If the number asked for does not match the number returned, return S_FALSE
            return (count == celt) ? VSConstants.S_OK : VSConstants.S_FALSE;
        }

        public virtual int get_ProjectCfg(out IVsProjectCfg2 ppIVsProjectCfg2)
        {
            ppIVsProjectCfg2 = (IVsProjectCfg2)this._projectCfg;
            return VSConstants.S_OK;
        }

        public virtual int get_Property(string pszProperty, out object pvar)
        {
            pvar = _project.GetProjectProperty(pszProperty);
            return VSConstants.S_OK;
        }

        #endregion
    }
}