<?xml version="1.0" encoding="utf-8"?>
<!--
/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt VS Tools.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
-->

<!--
///////////////////////////////////////////////////////////////////////////////////////////////////
// Helper inline tasks used by the Qt/MSBuild targets
// -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// TASK GetItemHash
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // Calculate an hash code (Deflate + Base64) for an item, given a list of metadata to use as key
  // Parameters:
  //      in ITaskItem Item: item for which the hash will be calculated
  //      in string[]  Keys: list of names of the metadata to use as item key
  //     out string    Hash: hash code (Base64 representation of Deflate'd UTF-8 item key)
  // -->
  <UsingTask TaskName="GetItemHash"
    TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <Item               ParameterType="Microsoft.Build.Framework.ITaskItem" Required="true" />
      <Keys               ParameterType="System.String[]"                     Required="true" />
      <Hash Output="true" ParameterType="System.String" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.Text"/>
      <Using Namespace="System.IO"/>
      <Using Namespace="System.IO.Compression"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
            var data = Encoding.UTF8.GetBytes(string.Concat(Keys.OrderBy(x => x)
                .Select(x => string.Format("[{0}={1}]", x, Item.GetMetadata(x))))
                .ToUpper());
            using (var dataZipped = new MemoryStream()) {
                using (var zip = new DeflateStream(dataZipped, CompressionLevel.Fastest))
                    zip.Write(data, 0, data.Length);
                Hash = Convert.ToBase64String(dataZipped.ToArray());
            }
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// TASK Expand
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // Expand a list of items, taking additional metadata from a base item and from a template item.
  // Parameters:
  //      in ITaskItem[] Items:    items to expand
  //      in ITaskItem   BaseItem: base item from which the expanded items derive
  //      in ITaskItem   Template = null: (optional) template containing metadata to add / update
  //     out ITaskItem[] Result:   list of new items
  // -->
  <UsingTask TaskName="Expand"
    TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <Items                ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <BaseItem             ParameterType="Microsoft.Build.Framework.ITaskItem"   Required="true" />
      <Template             ParameterType="Microsoft.Build.Framework.ITaskItem"   Required="false"/>
      <Result Output="true" ParameterType="Microsoft.Build.Framework.ITaskItem[]" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.Text.RegularExpressions"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
            Result = new ITaskItem[] { };
            var reserved = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
            {
                "AccessedTime", "CreatedTime", "DefiningProjectDirectory",
                "DefiningProjectExtension", "DefiningProjectFullPath", "DefiningProjectName",
                "Directory", "Extension", "Filename", "FullPath", "Identity", "ModifiedTime",
                "RecursiveDir", "RelativeDir", "RootDir",
            };
            var newItems = new List<ITaskItem>();
            foreach (var item in Items) {
                var newItem = new TaskItem(item);
                if (BaseItem != null)
                    BaseItem.CopyMetadataTo(newItem);
                var itemExt = newItem.GetMetadata("Extension");
                if (!string.IsNullOrEmpty(itemExt))
                    newItem.SetMetadata("Suffix", itemExt.Substring(1));
                if (Template != null) {
                    var metadataNames = Template.MetadataNames
                        .Cast<string>().Where(x => !reserved.Contains(x));
                    foreach (var metadataName in metadataNames) {
                        var metadataValue = Template.GetMetadata(metadataName);
                        newItem.SetMetadata(metadataName,
                            Regex.Replace(metadataValue, @"(%<)(\w+)(>)",
                            match => newItem.GetMetadata(match.Groups[2].Value)));
                    }
                }
                newItems.Add(newItem);
            }
            Result = newItems.ToArray();
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// TASK DumpItems
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // Dump contents of items as a log message. The contents are formatted as XML.
  // Parameters:
  //      in string       ItemType:     type of the items; this is used as the parent node of each
  //                                    item dump
  //      in ITaskItem[]  Items:        items to dump
  //      in bool         DumpReserved: include MSBuild reserved metadata in dump?
  //      in string       Metadata:     list of names of metadata to include in dump; omit to
  //                                    include all metadata
  // -->
  <UsingTask TaskName="DumpItems"
    TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <ItemType     ParameterType="System.String"                         Required="true" />
      <Items        ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <DumpReserved ParameterType="System.Boolean"                        Required="false" />
      <Metadata     ParameterType="System.String"                         Required="false" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.Diagnostics"/>
      <Using Namespace="System.Text"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
            var reserved = new HashSet<string>
            {
                "AccessedTime", "CreatedTime", "DefiningProjectDirectory",
                "DefiningProjectExtension", "DefiningProjectFullPath", "DefiningProjectName",
                "Directory", "Extension", "Filename", "FullPath", "Identity", "ModifiedTime",
                "RecursiveDir", "RelativeDir", "RootDir",
            };
            if (Metadata == null)
                Metadata = "";
            var requestedNames = new HashSet<string>(Metadata.Split(new[] { ';' },
                StringSplitOptions.RemoveEmptyEntries));
            var itemXml = new StringBuilder();
            if (Items.Any()) {
                foreach (var item in Items) {
                    if (itemXml.Length > 0)
                        itemXml.Append("\r\n");
                    itemXml.AppendFormat("<{0} Include=\"{1}\"", ItemType, item.ItemSpec);
                    var names = item.MetadataNames.Cast<string>()
                        .Where(x => (DumpReserved || !reserved.Contains(x))
                        && (!requestedNames.Any() || requestedNames.Contains(x)))
                        .OrderBy(x => x);
                    if (names.Any()) {
                        itemXml.Append(">\r\n");
                        foreach (string name in names) {
                            if (!DumpReserved && reserved.Contains(name))
                                continue;
                            if (!item.MetadataNames.Cast<string>().Contains(name))
                                continue;
                            var value = item.GetMetadata(name);
                            if (!string.IsNullOrEmpty(value))
                                itemXml.AppendFormat("  <{0}>{1}</{0}>\r\n", name, value);
                            else
                                itemXml.AppendFormat("  <{0}/>\r\n", name);
                        }
                        itemXml.AppendFormat("</{0}>", ItemType);
                    } else {
                        itemXml.Append("/>");
                    }
                }
            } else {
                itemXml.AppendFormat("<{0}/>", ItemType);
            }
            Log.LogMessage(MessageImportance.High, itemXml.ToString());
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// TASK QtRunWork
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // Run work items in parallel processes.
  // Parameters:
  //      in ITaskItem[] QtWork:      work items
  //      in int         QtMaxProcs:  maximum number of processes to run in parallel
  //      in bool        QtDebug:     generate debug messages
  //     out ITaskItem[] Result:      list of new items with the result of each work item
  // -->
  <UsingTask
    TaskName="QtRunWork"
    TaskFactory="CodeTaskFactory"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <QtWork               ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <QtMaxProcs           ParameterType="System.Int32"                          Required="true" />
      <QtDebug              ParameterType="System.Boolean"                        Required="true" />
      <Result Output="true" ParameterType="Microsoft.Build.Framework.ITaskItem[]" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.IO"/>
      <Using Namespace="System.Diagnostics"/>
      <Using Namespace="System.Collections.Generic"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
            Result = new ITaskItem[] { };
            bool ok = true;
            var Comparer = StringComparer.InvariantCultureIgnoreCase;
            var Comparison = StringComparison.InvariantCultureIgnoreCase;

            // Work item key = "%(WorkType)(%(Identity))"
            Func<string, string, string> KeyString = (x, y) => string.Format("{0}{{{1}}}", x, y);
            Func<ITaskItem, string> Key = (item) =>
                KeyString(item.GetMetadata("WorkType"), item.ItemSpec);
            var workItemKeys = new HashSet<string>(QtWork.Select(x => Key(x)), Comparer);

            // Work items, indexed by %(Identity)
            var workItemsByIdentity = QtWork
                .GroupBy(x => x.ItemSpec, x => Key(x), Comparer)
                .ToDictionary(x => x.Key, x => new List<string>(x), Comparer);

            // Work items, indexed by work item key
            var workItems = QtWork.Select(x => new
            {
                Self = x,
                Key = Key(x),
                ToolPath = x.GetMetadata("ToolPath"),
                Message = x.GetMetadata("Message"),
                DependsOn = new HashSet<string>(comparer: Comparer,
                    collection: x.GetMetadata("DependsOn")
                        .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
                        .Where(y => workItemsByIdentity.ContainsKey(y))
                        .SelectMany(y => workItemsByIdentity[y])
                    .Union(x.GetMetadata("DependsOnWork")
                        .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
                        .Select(y => KeyString(y, x.ItemSpec))
                        .Where(y => workItemKeys.Contains(y)))
                    .GroupBy(y => y, Comparer).Select(y => y.Key)
                    .Where(y => !y.Equals(Key(x), Comparison))),
                ProcessStartInfo = new ProcessStartInfo
                {
                    FileName = x.GetMetadata("ToolPath"),
                    Arguments = x.GetMetadata("Options"),
                    CreateNoWindow = true,
                    UseShellExecute = false,
                    RedirectStandardError = true,
                    RedirectStandardOutput = true,
                },
            })
            // In case of items with duplicate keys, use only the first one
            .GroupBy(x => x.Key, Comparer)
            .ToDictionary(x => x.Key, x => x.First(), Comparer);

            // Result
            var result = workItems.Values
                .ToDictionary(x => x.Key, x => new TaskItem(x.Self));

            // Dependency relation [item -> dependent items]
            var dependentsOf = workItems.Values
                .Where(x => x.DependsOn.Any())
                .SelectMany(x => x.DependsOn.Select(y => new { Dependent = x.Key, Dependency = y }))
                .GroupBy(x => x.Dependency, x => x.Dependent, Comparer)
                .ToDictionary(x => x.Key, x => new List<string>(x), Comparer);

            // Work items that are ready to start; initially queue all independent items
            var workQueue = new Queue<string>(workItems.Values
                .Where(x => !x.DependsOn.Any())
                .Select(x => x.Key));

            if (QtDebug) {
                Log.LogMessage(MessageImportance.High,
                    string.Format("## QtRunWork queueing\r\n##    {0}",
                    string.Join("\r\n##    ", workQueue)));
            }

            // Postponed items; save dependent items to queue later when ready
            var postponedItems = new HashSet<string>(workItems.Values
                .Where(x => x.DependsOn.Any())
                .Select(x => x.Key));

            if (QtDebug && postponedItems.Any()) {
                Log.LogMessage(MessageImportance.High,
                    string.Format("## QtRunWork postponed dependents\r\n##    {0}",
                    string.Join("\r\n##    ", postponedItems
                        .Select(x => string.Format("{0} <- {1}", x,
                                     string.Join(", ", workItems[x].DependsOn))))));
            }

            // Work items that are running; must synchronize with the exit of all processes
            var running = new Queue<KeyValuePair<string, Process>>();

            // Work items that have terminated
            var terminated = new HashSet<string>(Comparer);

            // While there are work items queued, start a process for each item
            while (ok && workQueue.Any()) {

                var workItem = workItems[workQueue.Dequeue()];
                Log.LogMessage(MessageImportance.High, workItem.Message);

                try {
                    var proc = Process.Start(workItem.ProcessStartInfo);
                    proc.OutputDataReceived += (object sender, DataReceivedEventArgs e) =>
                    {
                        if (!string.IsNullOrEmpty(e.Data))
                            Log.LogMessage(MessageImportance.High, string.Join(" ", new[]
                            {
                                (QtDebug ? "[" + (((Process)sender).Id.ToString()) + "]" : ""),
                                e.Data
                            }));
                    };
                    proc.ErrorDataReceived += (object sender, DataReceivedEventArgs e) =>
                    {
                        if (!string.IsNullOrEmpty(e.Data))
                            Log.LogMessage(MessageImportance.High, string.Join(" ", new[]
                            {
                                (QtDebug ? "[" + (((Process)sender).Id.ToString()) + "]" : ""),
                                e.Data
                            }));
                    };
                    proc.BeginOutputReadLine();
                    proc.BeginErrorReadLine();
                    running.Enqueue(new KeyValuePair<string, Process>(workItem.Key, proc));
                } catch (Exception e) {
                    Log.LogMessage(MessageImportance.High,
                        string.Format("[QtRunWork] Error starting process {0}: {1}",
                        workItem.ToolPath, e.Message));
                    ok = false;
                }

                string qtDebugRunning = "";
                if (QtDebug) {
                    qtDebugRunning = string.Format("## QtRunWork waiting {0}",
                        string.Join(", ", running
                            .Select(x => string.Format("{0} [{1}]", x.Key, x.Value.Id))));
                }

                // Wait for process to terminate when there are processes running, and...
                while (ok && running.Any()
                    // ...work is queued but already reached the maximum number of processes, or...
                    && ((workQueue.Any() && running.Count >= QtMaxProcs)
                    // ...work queue is empty but there are dependents that haven't yet been queued
                    || (!workQueue.Any() && postponedItems.Any()))) {

                    var itemProc = running.Dequeue();
                    workItem = workItems[itemProc.Key];
                    var proc = itemProc.Value;

                    if (QtDebug && !string.IsNullOrEmpty(qtDebugRunning)) {
                        Log.LogMessage(MessageImportance.High, qtDebugRunning);
                        qtDebugRunning = "";
                    }

                    if (proc.WaitForExit(100)) {
                        if (QtDebug) {
                            Log.LogMessage(MessageImportance.High,
                                string.Format("## QtRunWork exit {0} [{1}] = {2} ({3:0.00} msecs)",
                                workItem.Key, proc.Id, proc.ExitCode,
                                (proc.ExitTime - proc.StartTime).TotalMilliseconds));
                        }

                        // Process terminated; check exit code and close
                        terminated.Add(workItem.Key);
                        result[workItem.Key].SetMetadata("ExitCode", proc.ExitCode.ToString());
                        ok &= (proc.ExitCode == 0);
                        proc.Close();

                        // Add postponed dependent items to work queue
                        if (ok && dependentsOf.ContainsKey(workItem.Key)) {
                            // Dependents of workItem...
                            var readyDependents = dependentsOf[workItem.Key]
                                // ...that have not yet been queued...
                                .Where(x => postponedItems.Contains(x)
                                    // ...and whose dependending items have all terminated.
                                    && workItems[x].DependsOn.All(y => terminated.Contains(y)));

                            if (QtDebug && readyDependents.Any()) {
                                Log.LogMessage(MessageImportance.High,
                                string.Format("## QtRunWork queueing\r\n##    {0}",
                                string.Join("\r\n##    ", readyDependents)));
                            }

                            foreach (var dependent in readyDependents) {
                                postponedItems.Remove(dependent);
                                workQueue.Enqueue(dependent);
                            }
                        }
                    } else {
                        // Process is still running; feed it back into the running queue
                        running.Enqueue(itemProc);
                    }
                }
            }

            // If there are items still haven't been queued, that means a circular dependency exists
            if (ok && postponedItems.Any()) {
                ok = false;
                Log.LogMessage(MessageImportance.High, "[QtRunWork] Error: circular dependency");
                if (QtDebug) {
                    Log.LogMessage(MessageImportance.High,
                        string.Format("## QtRunWork circularity\r\n##    {0}",
                        string.Join("\r\n##    ", postponedItems
                            .Select(x => string.Format("{0} <- {1}", x,
                                         string.Join(", ", workItems[x].DependsOn))))));
                }
            }

            if (ok && QtDebug) {
                Log.LogMessage(MessageImportance.High,
                    "## QtRunWork all work queued");
                if (running.Any()) {
                    Log.LogMessage(MessageImportance.High,
                        string.Format("## QtRunWork waiting {0}",
                        string.Join(", ", running
                            .Select(x => string.Format("{0} [{1}]", x.Key, x.Value.Id)))));
                }
            }

            // Wait for all running processes to terminate
            while (running.Any()) {
                var itemProc = running.Dequeue();
                var workItem = workItems[itemProc.Key];
                var proc = itemProc.Value;
                if (proc.WaitForExit(100)) {
                    if (QtDebug) {
                        Log.LogMessage(MessageImportance.High,
                            string.Format("## QtRunWork exit {0} [{1}] = {2} ({3:0.00} msecs)",
                            workItem.Key, proc.Id, proc.ExitCode,
                            (proc.ExitTime - proc.StartTime).TotalMilliseconds));
                    }
                    // Process terminated; check exit code and close
                    result[workItem.Key].SetMetadata("ExitCode", proc.ExitCode.ToString());
                    ok &= (proc.ExitCode == 0);
                    proc.Close();
                } else {
                    // Process is still running; feed it back into the running queue
                    running.Enqueue(itemProc);
                }
            }

            if (QtDebug) {
                Log.LogMessage(MessageImportance.High,
                    string.Format("## QtRunWork result {0}", (ok ? "ok" : "FAILED!")));
            }

            Result = result.Values.ToArray();
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// TASK ParseVarDefs
  /////////////////////////////////////////////////////////////////////////////////////////////////
  //
  // -->
  <UsingTask TaskName="ParseVarDefs"
    TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <QtVars Required="true"
        ParameterType="System.String"/>
      <OutVarDefs Output="true"
        ParameterType="Microsoft.Build.Framework.ITaskItem[]"/>
    </ParameterGroup>
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.Text"/>
      <Using Namespace="System.Text.RegularExpressions"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
            OutVarDefs = Regex.Matches(QtVars,
                @"\s*(\w+)\s*(?:;|=\s*(\w*)\s*(?:\/((?:\\.|[^;\/])*)\/((?:\\.|[^;\/])*)\/)?)?")
                .Cast<Match>()
                .Where((Match x) => x.Groups.Count > 4 && !string.IsNullOrEmpty(x.Groups[1].Value))
                .Select((Match x) => x.Groups
                      .Cast<Group>()
                      .Select((Group y) => !string.IsNullOrEmpty(y.Value) ? y.Value : null)
                      .ToArray())
                .Select((string[] x) => new TaskItem(x[1],
                    new Dictionary<string,string>
                    {
                        { "Name" ,    x[2] ?? x[1] },
                        { "Pattern" , x[3] ?? ".*" },
                        { "Value" ,   x[4] ?? "$0" },
                    }))
                .ToArray();
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// TASK GetVarsFromMakefile
  /////////////////////////////////////////////////////////////////////////////////////////////////
  //
  // -->
  <UsingTask TaskName="GetVarsFromMakefile"
    TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <Makefile Required="true"
        ParameterType="System.String"/>
      <VarDefs Required="false"
        ParameterType="Microsoft.Build.Framework.ITaskItem[]"/>
      <OutVars Output="true"
        ParameterType="Microsoft.Build.Framework.ITaskItem[]"/>
    </ParameterGroup>
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.Text"/>
      <Using Namespace="System.Text.RegularExpressions"/>
      <Using Namespace="System.IO"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
            var makefileVars = Regex.Matches(
                File.ReadAllText(Makefile),
                @"^(\w+)[^\=\r\n\S]*\=[^\r\n\S]*([^\r\n]+)[\r\n]",
                RegexOptions.Multiline).Cast<Match>()
                .Where(x => x.Groups.Count > 2 && x.Groups[1].Success && x.Groups[2].Success)
                .ToDictionary(x => x.Groups[1].Value, x => x.Groups[2].Value);
            OutVars = VarDefs
                .Where(x => makefileVars.ContainsKey(x.GetMetadata("Name")))
                .Select(x => new TaskItem(x.ItemSpec, new Dictionary<string,string>
                { {
                    "Value",
                    string.Join(";", Regex
                        .Matches(makefileVars[x.GetMetadata("Name")], x.GetMetadata("Pattern"))
                        .Cast<Match>()
                        .Select(y => Regex
                            .Replace(y.Value, x.GetMetadata("Pattern"), x.GetMetadata("Value")))
                        .Where(y => !string.IsNullOrEmpty(y))
                        .ToHashSet())
                } }))
                .Where(x => !string.IsNullOrEmpty(x.GetMetadata("Value")))
                .ToArray();
        ]]>
      </Code>
    </Task>
  </UsingTask>
</Project>
