<?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>

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// TASK ListQrc
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // List resource paths in a QRC file.
  // Parameters:
  //      in string    QrcFilePath: path to QRC file
  //     out string[]  Result: paths to files referenced in QRC
  // -->
  <UsingTask TaskName="ListQrc"
    TaskFactory="CodeTaskFactory"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <QrcFilePath ParameterType="System.String"   Required="true" />
      <Result      ParameterType="System.String[]" Output="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.Xml"/>
      <Reference Include="System.Xml.Linq"/>
      <Using Namespace="System.Xml.Linq"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
            XDocument qrc = XDocument.Load(QrcFilePath, LoadOptions.SetLineInfo);
            IEnumerable<XElement> files = qrc
                .Element("RCC")
                .Elements("qresource")
                .Elements("file");
            Uri QrcPath = new Uri(QrcFilePath);
            Result = files
                .Select(x => new Uri(QrcPath, x.Value).LocalPath)
                .ToArray();
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// 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.LogError(
                        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.LogError("[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();
            if (!ok)
                return false;
        ]]>
      </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[]"/>
      <ExcludeValues Required="true"
        ParameterType="System.String[]"/>
      <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
                    && !string.IsNullOrEmpty(x.Groups[1].Value))
                .GroupBy(x => x.Groups[1].Value)
                .ToDictionary(g => g.Key, g => g.Last().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)
                            && !ExcludeValues.Contains(y,
                                StringComparer.InvariantCultureIgnoreCase))
                        .ToHashSet())
                } }))
                .Where(x => !string.IsNullOrEmpty(x.GetMetadata("Value")))
                .ToArray();
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// TASK Flatten
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // Destructure items into a "flat" list of metadata. The output is a list of (Name, Value) pairs,
  // each corresponding to one item metadata. Semi-colon-separated lists will also expand to many
  // items in the output list, with the metadata name shared among them.
  // Example:
  //     INPUT:
  //         <QtMoc>
  //           <InputFile>foo.h</InputFile>
  //           <IncludePath>C:\FOO;D:\BAR</IncludePath>
  //         </QtMoc>
  //     OUTPUT:
  //         <Result>
  //           <Name>InputFile</Name>
  //           <Value>foo.h</Value>
  //         </Result>
  //         <Result>
  //           <Name>IncludePath</Name>
  //           <Value>C:\FOO</Value>
  //         </Result>
  //         <Result>
  //           <Name>IncludePath</Name>
  //           <Value>D:\BAR</Value>
  //         </Result>
  // Parameters:
  //      in ITaskItem[] Items:    list of items to flatten
  //      in string[]    Metadata: names of metadata to look for; omit to include all metadata
  //     out ITaskItem[] Result:   list of metadata from all items
  // -->
  <UsingTask TaskName="Flatten"
    TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <Items    ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <Metadata ParameterType="System.String[]"                       Required="false" />
      <Result   ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
    </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[] { };
            var reserved = new HashSet<string>
            {
                "AccessedTime", "CreatedTime", "DefiningProjectDirectory",
                "DefiningProjectExtension", "DefiningProjectFullPath", "DefiningProjectName",
                "Directory", "Extension", "Filename", "FullPath", "Identity", "ModifiedTime",
                "RecursiveDir", "RelativeDir", "RootDir",
            };
            if (Metadata == null)
                Metadata = new string[0];
            var requestedNames = new HashSet<string>(Metadata.Where(x => !string.IsNullOrEmpty(x)));
            var newItems = new List<ITaskItem>();
            foreach (var item in Items) {
                var itemName = item.ItemSpec;
                var names = item.MetadataNames.Cast<string>().Where(x => !reserved.Contains(x)
                    && (!requestedNames.Any() || requestedNames.Contains(x)));
                foreach (string name in names) {
                    var values = item.GetMetadata(name).Split(';');
                    foreach (string value in values.Where(v => !string.IsNullOrEmpty(v))) {
                        newItems.Add(new TaskItem(string.Format("{0}={1}", name, value),
                            new Dictionary<string, string>
                            {
                                { "Item",  itemName },
                                { "Name",  name },
                                { "Value", value },
                            }));
                    }
                }
            }
            Result = newItems.ToArray();
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// TASK HostTranslatePaths
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // Translate local (Windows) paths to build host paths. This could be a Linux host for cross
  // compilation, or a simple copy (i.e. "no-op") when building in Windows.
  // Input and output items are in the form:
  //    <...>
  //      <Item>...</Item>
  //      <Name>...</Name>
  //      <Value>...</Value>
  //    </...>
  // where <Item> is the local path, <Name> is a filter criteria identifier matched with the Names
  // parameter, and <Value> is set to the host path in output items (for input items <Value> must
  // be equal to <Item>).
  // Parameters:
  //      in ITaskItem[] Items:  input items with local paths
  //      in string[]    Names:  filter criteria; unmatched items will simply be copied (i.e. no-op)
  //     out ITaskItem[] Result: output items with translated host paths
  // -->
  <!--// Linux build over WSL -->
  <UsingTask TaskName="HostTranslatePaths" TaskFactory="CodeTaskFactory"
    Condition="'$(ApplicationType)' == 'Linux' AND '$(PlatformToolset)' == 'WSL_1_0'"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <Items  ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <Names  ParameterType="System.String[]"                       Required="false" />
      <Result ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="$(VCTargetsPath)\Application Type\Linux\1.0\liblinux.dll"/>
      <Using Namespace="System.IO"/>
      <Using Namespace="liblinux"/>
      <Using Namespace="liblinux.IO"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          Result = new ITaskItem[] { };
          var newItems = new List<ITaskItem>();
          foreach (var item in Items) {
              string itemName = item.GetMetadata("Name");
              string itemValue = item.GetMetadata("Value");
              if (Names.Contains(itemName)) {
                  if (Path.IsPathRooted(itemValue) && !itemValue.StartsWith("/"))
                      itemValue = PathUtils.TranslateWindowsPathToWSLPath(itemValue);
                  else
                      itemValue = itemValue.Replace(@"\", "/");
              }
              newItems.Add(new TaskItem(item.ItemSpec,
                  new Dictionary<string, string>
                  {
                      { "Item",  item.GetMetadata("Item") },
                      { "Name",  itemName },
                      { "Value", itemValue },
                  }));
          }
          Result = newItems.ToArray();
        ]]>
      </Code>
    </Task>
  </UsingTask>
  <!--// Linux build over SSH -->
  <UsingTask TaskName="HostTranslatePaths" TaskFactory="CodeTaskFactory"
    Condition="'$(ApplicationType)' == 'Linux' AND '$(PlatformToolset)' != 'WSL_1_0'"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <Items  ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <Names  ParameterType="System.String[]"                       Required="false" />
      <Result ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
    </ParameterGroup>
    <Task>
      <Reference
        Include="$(VCTargetsPath)\Application Type\Linux\1.0\Microsoft.Build.Linux.Tasks.dll"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          Result = new ITaskItem[] { };
          var newItems = new List<ITaskItem>();
          foreach (var item in Items) {
              string itemName = item.GetMetadata("Name");
              string itemValue = item.GetMetadata("Value");
              if (Names.Contains(itemName)) {
                  if (Path.IsPathRooted(itemValue) && !itemValue.StartsWith("/")) {
                      var projectdir = new Uri(@"$(ProjectDir)");
                      var itemFileName = Path.GetFileName(itemValue);
                      var itemDirName = Path.GetFullPath(Path.GetDirectoryName(itemValue));
                      if (!itemDirName.EndsWith(@"\"))
                          itemDirName += @"\";
                      var itemDir = new Uri(itemDirName);
                      if (projectdir.IsBaseOf(itemDir)) {
                          itemValue = projectdir.MakeRelativeUri(itemDir).OriginalString
                            + itemFileName;
                      } else {
                          Log.LogWarning("Unable to translate path: {0}", itemValue);
                      }
                  } else {
                      itemValue = itemValue.Replace(@"\", "/");
                  }
              }
              newItems.Add(new TaskItem(item.ItemSpec,
                  new Dictionary<string, string>
                  {
                      { "Item",  item.GetMetadata("Item") },
                      { "Name",  itemName },
                      { "Value", itemValue },
                  }));
          }
          Result = newItems.ToArray();
        ]]>
      </Code>
    </Task>
  </UsingTask>
  <!--// Local (Windows) build -->
  <UsingTask TaskName="HostTranslatePaths" TaskFactory="CodeTaskFactory"
    Condition="'$(ApplicationType)' != 'Linux'"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <Items  ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
      <Names  ParameterType="System.String[]"                       Required="false" />
      <Result ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
    </ParameterGroup>
    <Task>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          Result = Items.ToArray();
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!--
  /////////////////////////////////////////////////////////////////////////////////////////////////
  /// TASK HostExec
  /////////////////////////////////////////////////////////////////////////////////////////////////
  // Run command in build host.
  // Parameters:
  //     in string      Command: Command to run on the build host
  //     in string      RedirectStdOut: Path to file to receive redirected output messages
  //                      * can be NUL to discard messages
  //     in string      RedirectStdErr: Path to file to receive redirected error messages
  //                      * can be NUL to discard messages
  //                      * can be STDOUT to apply the same redirection as output messages
  //     in string      WorkingDirectory: Path to directory where command will be run
  //     in ITaskItem[] Inputs: List of local -> host path mappings for command inputs
  //                      * item format: cf. HostTranslatePaths task
  //     in ITaskItem[] Outputs: List of host -> local path mappings for command outputs
  //                      * item format: cf. HostTranslatePaths task
  //     in string      RemoteTarget: Set by ResolveRemoteDir in SSH mode; null otherwise
  //     in string      RemoteProjectDir: Set by ResolveRemoteDir in SSH mode; null otherwise
  //     in bool        IgnoreExitCode: Set flag to disable build error if command failed
  //    out int         ExitCode: status code at command exit
  // -->
  <!--// Linux build over WSL -->
  <UsingTask TaskName="HostExec" TaskFactory="CodeTaskFactory"
    Condition="'$(ApplicationType)' == 'Linux' AND '$(PlatformToolset)' == 'WSL_1_0'"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <!--IN-->
      <Message          ParameterType="System.String"                         Required="false" />
      <Command          ParameterType="System.String"                         Required="true"  />
      <RedirectStdOut   ParameterType="System.String"                         Required="false" />
      <RedirectStdErr   ParameterType="System.String"                         Required="false" />
      <WorkingDirectory ParameterType="System.String"                         Required="false" />
      <Inputs           ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" />
      <Outputs          ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" />
      <RemoteTarget     ParameterType="System.String"                         Required="false" />
      <RemoteProjectDir ParameterType="System.String"                         Required="false" />
      <IgnoreExitCode   ParameterType="System.Boolean"                        Required="false" />
      <!--OUT-->
      <ExitCode         ParameterType="System.Int32"                          Output="true" />
    </ParameterGroup>
    <Task>
      <Reference
        Include="$(VCTargetsPath)\Application Type\Linux\1.0\Microsoft.Build.Linux.Tasks.dll"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          if (!string.IsNullOrEmpty(Message))
              Log.LogMessage(MessageImportance.High, Message);
          Command = "(" + Command + ")";
          if (RedirectStdOut == "NUL" || RedirectStdOut == "/dev/null")
            Command += " 1> /dev/null";
          else if (!string.IsNullOrEmpty(RedirectStdOut))
            Command += " 1> " + RedirectStdOut;
          if (RedirectStdErr == "NUL" || RedirectStdErr == "/dev/null")
            Command += " 2> /dev/null";
          else if (RedirectStdErr == "STDOUT")
            Command += " 2>&1";
          else if (!string.IsNullOrEmpty(RedirectStdErr))
            Command += " 2> " + RedirectStdErr;

          var createDirs = new List<string>();
          if (Inputs != null) {
              createDirs.AddRange(Inputs
                  .Select(x => string.Format("\x24(dirname {0})", x.GetMetadata("Value"))));
          }
          if (Outputs != null) {
              createDirs.AddRange(Outputs
                  .Select(x => string.Format("\x24(dirname {0})", x.GetMetadata("Value"))));
          }
          if (!string.IsNullOrEmpty(WorkingDirectory)) {
              createDirs.Add(WorkingDirectory);
              Command = string.Format("cd {0}; {1}", WorkingDirectory, Command);
          }
          if (createDirs.Any()) {
              Command = string.Format("{0}; {1}",
                  string.Join("; ", createDirs.Select(x => string.Format("mkdir -p {0}", x))),
                  Command);
          }

          var taskExec = new Microsoft.Build.Linux.WSL.Tasks.ExecuteCommand()
          {
              BuildEngine = BuildEngine,
              HostObject = HostObject,
              ProjectDir = @"$(ProjectDir)",
              IntermediateDir = @"$(IntDir)",
              WSLPath = @"$(WSLPath)",
              Command = Command,
          };
          Log.LogMessage("\r\n==== HostExec: Microsoft.Build.Linux.WSL.Tasks.ExecuteCommand");
          Log.LogMessage("ProjectDir: {0}", taskExec.ProjectDir);
          Log.LogMessage("IntermediateDir: {0}", taskExec.IntermediateDir);
          Log.LogMessage("WSLPath: {0}", taskExec.WSLPath);
          Log.LogMessage("Command: {0}", taskExec.Command);

          bool ok = taskExec.Execute();
          Log.LogMessage("== {0} ExitCode: {1}\r\n", ok ? "OK" : "FAIL", taskExec.ExitCode);

          ExitCode = taskExec.ExitCode;
          if (!ok && !IgnoreExitCode) {
              Log.LogError("Host command failed.");
              return false;
          }
        ]]>
      </Code>
    </Task>
  </UsingTask>
  <!--// Linux build over SSH -->
  <UsingTask TaskName="HostExec" TaskFactory="CodeTaskFactory"
    Condition="'$(ApplicationType)' == 'Linux' AND '$(PlatformToolset)' != 'WSL_1_0'"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <!--IN-->
      <Message          ParameterType="System.String"                         Required="false" />
      <Command          ParameterType="System.String"                         Required="true"  />
      <RedirectStdOut   ParameterType="System.String"                         Required="false" />
      <RedirectStdErr   ParameterType="System.String"                         Required="false" />
      <WorkingDirectory ParameterType="System.String"                         Required="false" />
      <Inputs           ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" />
      <Outputs          ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" />
      <RemoteTarget     ParameterType="System.String"                         Required="false" />
      <RemoteProjectDir ParameterType="System.String"                         Required="false" />
      <IgnoreExitCode   ParameterType="System.Boolean"                        Required="false" />
      <!--OUT-->
      <ExitCode         ParameterType="System.Int32"                          Output="true" />
    </ParameterGroup>
    <Task>
      <Reference
          Include="$(VCTargetsPath)\Application Type\Linux\1.0\Microsoft.Build.Linux.Tasks.dll"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          if (!string.IsNullOrEmpty(Message))
              Log.LogMessage(MessageImportance.High, Message);
          var createDirs = new List<string>
          {
              string.Format("{0}/{1}", RemoteProjectDir, WorkingDirectory)
          };

          var localFilesToCopyRemotelyMapping = new string[0];
          if (Inputs != null) {
              localFilesToCopyRemotelyMapping = Inputs
                  .Select(x => string.Format(@"{0}:={1}/{2}",
                      x.GetMetadata("Item"),
                      RemoteProjectDir,
                      x.GetMetadata("Value")))
                  .ToArray();
              createDirs.AddRange(Inputs
                  .Select(x => string.Format("\x24(dirname {0})", x.GetMetadata("Value"))));
          }

          var remoteFilesToCopyLocallyMapping = new string[0];
          if (Outputs != null) {
              remoteFilesToCopyLocallyMapping = Outputs
                .Select(x => string.Format(@"{0}/{1}:={2}",
                    RemoteProjectDir,
                    x.GetMetadata("Value"),
                    x.GetMetadata("Item")))
                .ToArray();
              createDirs.AddRange(Outputs
                  .Select(x => string.Format("\x24(dirname {0})", x.GetMetadata("Value"))));
          }

          Command = "(" + Command + ")";
          if (RedirectStdOut == "NUL" || RedirectStdOut == "/dev/null")
            Command += " 1> /dev/null";
          else if (!string.IsNullOrEmpty(RedirectStdOut))
            Command += " 1> " + RedirectStdOut;
          if (RedirectStdErr == "NUL" || RedirectStdErr == "/dev/null")
            Command += " 2> /dev/null";
          else if (RedirectStdErr == "STDOUT")
            Command += " 2>&1";
          else if (!string.IsNullOrEmpty(RedirectStdErr))
            Command += " 2> " + RedirectStdErr;
          Command = string.Format("cd {0}/{1}; {2}", RemoteProjectDir, WorkingDirectory, Command);

          var taskCopyFiles = new Microsoft.Build.Linux.Tasks.Execute()
          {
              BuildEngine = BuildEngine,
              HostObject = HostObject,
              ProjectDir = @"$(ProjectDir)",
              IntermediateDir = @"$(IntDir)",
              RemoteTarget = RemoteTarget,
              RemoteProjectDir = RemoteProjectDir,
              Command = string.Join("; ", createDirs.Select(x => string.Format("mkdir -p {0}", x))),
              LocalFilesToCopyRemotelyMapping = localFilesToCopyRemotelyMapping,
          };
          var taskExec = new Microsoft.Build.Linux.Tasks.Execute()
          {
              BuildEngine = BuildEngine,
              HostObject = HostObject,
              ProjectDir = @"$(ProjectDir)",
              IntermediateDir = @"$(IntDir)",
              RemoteTarget = RemoteTarget,
              RemoteProjectDir = RemoteProjectDir,
              Command = Command,
              RemoteFilesToCopyLocallyMapping = remoteFilesToCopyLocallyMapping,
          };

          Log.LogMessage("\r\n==== HostExec: Microsoft.Build.Linux.Tasks.Execute");
          Log.LogMessage("ProjectDir: {0}", taskExec.ProjectDir);
          Log.LogMessage("IntermediateDir: {0}", taskExec.IntermediateDir);
          Log.LogMessage("RemoteTarget: {0}", taskExec.RemoteTarget);
          Log.LogMessage("RemoteProjectDir: {0}", taskExec.RemoteProjectDir);
          if (taskExec.LocalFilesToCopyRemotelyMapping.Any())
              Log.LogMessage("LocalFilesToCopyRemotelyMapping: {0}",
                  taskExec.LocalFilesToCopyRemotelyMapping);
          if (taskExec.RemoteFilesToCopyLocallyMapping.Any())
              Log.LogMessage("RemoteFilesToCopyLocallyMapping: {0}",
                  taskExec.RemoteFilesToCopyLocallyMapping);
          Log.LogMessage("CreateDirs: {0}", taskCopyFiles.Command);
          Log.LogMessage("Command: {0}", taskExec.Command);

          if (!taskCopyFiles.ExecuteTool())
              return false;
          bool ok = taskExec.ExecuteTool();
          Log.LogMessage("== {0} ExitCode: {1}\r\n", ok ? "OK" : "FAIL", taskExec.ExitCode);

          ExitCode = taskExec.ExitCode;
          if (!ok && !IgnoreExitCode) {
              Log.LogError("Host command failed.");
              return false;
          }
        ]]>
      </Code>
    </Task>
  </UsingTask>
  <!--// Local (Windows) build -->
  <UsingTask TaskName="HostExec" TaskFactory="CodeTaskFactory"
    Condition="'$(ApplicationType)' != 'Linux'"
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
    <ParameterGroup>
      <!--IN-->
      <Message          ParameterType="System.String"                         Required="false" />
      <Command          ParameterType="System.String"                         Required="true"  />
      <RedirectStdOut   ParameterType="System.String"                         Required="false" />
      <RedirectStdErr   ParameterType="System.String"                         Required="false" />
      <WorkingDirectory ParameterType="System.String"                         Required="false" />
      <Inputs           ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" />
      <Outputs          ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="false" />
      <RemoteTarget     ParameterType="System.String"                         Required="false" />
      <RemoteProjectDir ParameterType="System.String"                         Required="false" />
      <IgnoreExitCode   ParameterType="System.Boolean"                        Required="false" />
      <!--OUT-->
      <ExitCode         ParameterType="System.Int32"                          Output="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll"/>
      <Reference Include="$(MSBuildToolsPath)\Microsoft.Build.Utilities.Core.dll"/>
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          if (!string.IsNullOrEmpty(Message))
              Log.LogMessage(MessageImportance.High, Message);
          Command = "(" + Command + ")";
          if (RedirectStdOut == "NUL" || RedirectStdOut == "/dev/null")
            Command += " 1> NUL";
          else if (!string.IsNullOrEmpty(RedirectStdOut))
            Command += " 1> " + RedirectStdOut;
          if (RedirectStdErr == "NUL" || RedirectStdErr == "/dev/null")
            Command += " 2> NUL";
          else if (RedirectStdErr == "STDOUT")
            Command += " 2>&1";
          else if (!string.IsNullOrEmpty(RedirectStdErr))
            Command += " 2> " + RedirectStdErr;

          var taskExec = new Microsoft.Build.Tasks.Exec()
          {
              BuildEngine = BuildEngine,
              HostObject = HostObject,
              WorkingDirectory = WorkingDirectory,
              Command = Command,
              IgnoreExitCode = IgnoreExitCode,
          };

          Log.LogMessage("\r\n==== HostExec: Microsoft.Build.Tasks.Exec");
          Log.LogMessage("WorkingDirectory: {0}", taskExec.WorkingDirectory);
          Log.LogMessage("Command: {0}", taskExec.Command);

          bool ok = taskExec.Execute();
          Log.LogMessage("== {0} ExitCode: {1}\r\n", ok ? "OK" : "FAIL", taskExec.ExitCode);

          ExitCode = taskExec.ExitCode;
          if (!ok)
              return false;
        ]]>
      </Code>
    </Task>
  </UsingTask>
</Project>
