<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- 7z Task -->
  <UsingTask TaskName="_7z"
             TaskFactory="CodeTaskFactory"
             AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <Inputs Required="true" ParameterType="System.String" />
      <Output Required="true" ParameterType="System.String" />
    </ParameterGroup>
    <Task>
      <Using Namespace="System"/>
      <Using Namespace="System.Diagnostics"/>
      <Using Namespace="System.IO"/>
      <Code Type="Method" Language="cs">
        <![CDATA[
          public override bool Execute()
          {
              string appPath = Find7zPath();
              if (appPath == null)
              {
                  Log.LogError("Unable to find 7z.exe.");
                  return false;
              }

              var argsSB = new StringBuilder();
              argsSB.Append("a -tzip -mx9 -mtc=off ");
              argsSB.Append("\"" + Output + "\" ");
              foreach (string input in Inputs.Split(';'))
              {
                  argsSB.Append("\"" + input + "\" ");
              }
              string args = argsSB.ToString();

              Log.LogMessage(MessageImportance.Normal, "7z " + args);

              var psi = new ProcessStartInfo(appPath, args);
              psi.CreateNoWindow = true;
              psi.UseShellExecute = false;
              psi.RedirectStandardOutput = true;
              psi.RedirectStandardError = true;
              var process = Process.Start(psi);
              process.WaitForExit();
              if (process.ExitCode != 0)
              {
                  string appError = process.StandardError.ReadToEnd();
                  appError = appError.Replace("\r\n", "\n");
                  appError = appError.Replace("\r", "\n");
                  appError = appError.Replace("\n", " ");
                  int colonIndex = appError.IndexOf(":");
                  if (colonIndex != -1)
                  {
                      appError = appError.Substring(colonIndex + 1);
                  }
                  appError = appError.Trim();
                  Log.LogError(appError);
                  return false;
              }

              string appOutput = process.StandardOutput.ReadToEnd();
              Log.LogMessage(MessageImportance.Normal, appOutput);
              return true;
          }

          private string Find7zPath()
          {
              const string DefaultAppFileName = "7z.exe";
              string DefaultAppPath = Path.Combine("7-Zip", DefaultAppFileName);

              // HACK needed as SpecialFolder.ProgramFiles returns x86 for a 32-bit process
              string programFiles = Path.Combine(Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)), "Program Files");
              string appPath = Path.Combine(programFiles, DefaultAppPath);
              if (File.Exists(appPath))
              {
                  return appPath;
              }

              programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
              appPath = Path.Combine(programFiles, DefaultAppPath);
              if (File.Exists(appPath))
              {
                  return appPath;
              }
              
              programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
              appPath = Path.Combine(programFiles, DefaultAppPath);
              if (File.Exists(appPath))
              {
                  return appPath;
              }
          
              string[] envPaths = Environment.GetEnvironmentVariable("PATH").Split(';');
              foreach (string envPath in envPaths)
              {
                  appPath = Path.Combine(envPath, DefaultAppFileName);
                  if (File.Exists(appPath))
                  {
                      return appPath;
                  }
              }
              
              return null;
          }
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!-- Unzip task -->
  <UsingTask TaskName="Unzip"
             TaskFactory="CodeTaskFactory"
             AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <Input Required="true" ParameterType="System.String" />
      <OutputDirectory Required="true" ParameterType="System.String" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.IO.Compression.FileSystem" />
      <Code Type="Fragment" Language="cs">
        <![CDATA[
          Log.LogMessage(MessageImportance.Normal, String.Format("Extracting '{0}' to '{1}'.", Input, OutputDirectory));
          System.IO.Compression.ZipFile.ExtractToDirectory(Input, OutputDirectory);
        ]]>
      </Code>
    </Task>
  </UsingTask>

  <!-- DownloadDependency task -->
  <UsingTask TaskName="DownloadDependency"
             TaskFactory="CodeTaskFactory"
             AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
      <Name            Required="true"  ParameterType="System.String" />
      <Url             Required="true"  ParameterType="System.String" />
      <Sha1            Required="true"  ParameterType="System.String" />
      <CheckFile       Required="false" ParameterType="System.String" />
      <OutputDirectory Required="true"  ParameterType="System.String" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.IO.Compression, Version=4.0.0.0" />
      <Reference Include="System.IO.Compression.FileSystem" />
      <Using Namespace="System"/>
      <Using Namespace="System.IO"/>
      <Using Namespace="System.IO.Compression"/>
      <Using Namespace="System.Net"/>
      <Using Namespace="System.Text"/>
      <Using Namespace="Microsoft.Build.Framework"/>
      <Using Namespace="Microsoft.Build.Utilities"/>
      <Code Type="Method" Language="cs">
        <![CDATA[
          public override bool Execute()
          {
              if (!String.IsNullOrEmpty(CheckFile))
              {
                  string checkSha1 = GetSha1FromCheckFile(CheckFile, Name);
                  if (String.Equals(checkSha1, Sha1, StringComparison.OrdinalIgnoreCase) && Directory.Exists(OutputDirectory))
                  {
                      Log.LogMessage(MessageImportance.Normal, String.Format("{0} up to date", Name));
                      return true;
                  }
              }

              string tempFile = Path.GetTempFileName();
              try
              {
                  // Download the file
                  Log.LogMessage(MessageImportance.Normal, String.Format("Downloading '{0}'.", Url));
                  var client = new WebClient();
                  client.DownloadFile(Url, tempFile);

                  // Check the file matches
                  string actualSha1;
                  if (!CheckFileSha1(tempFile, Sha1, out actualSha1))
                  {
                      Log.LogError("Download file did not match expected SHA1\n  expected: {0}\n  actual: {1}", Sha1, actualSha1);
                      return false;
                  }

                  // Extract contents
                  Log.LogMessage(MessageImportance.Normal, String.Format("Extracting to '{0}'.", OutputDirectory));
                  if (!Directory.Exists(OutputDirectory))
                  {
                      Directory.CreateDirectory(OutputDirectory);
                  }
                  ExtractZip(tempFile, OutputDirectory, overwrite: true);

                  SetSha1InCheckFile(CheckFile, Name, Sha1);
              }
              catch (Exception ex)
              {
                  Log.LogErrorFromException(ex, showStackTrace: false);
              }
              finally
              {
                  try
                  {
                      File.Delete(tempFile);
                  }
                  catch
                  {
                  }
              }

              return true;
          }

          private string GetSha1FromCheckFile(string checkFile, string name)
          {
              string result = null;
              try
              {
                  if (File.Exists(checkFile))
                  {
                      string[] lines = File.ReadAllLines(checkFile);
                      string sha1;
                      GetCheckFileLineIndexSha1(lines, name, out sha1);
                      return sha1;
                  }
              }
              catch (Exception ex)
              {
                  Log.LogWarningFromException(ex, showStackTrace: false);
              }
              return result;
          }

          private void SetSha1InCheckFile(string checkFile, string name, string sha1)
          {
              try
              {
                  string newLine = String.Format("{0} = {1}", name, sha1.ToLower());
                  string[] lines = new string[0];
                  int lineIndex = -1;
                  if (File.Exists(checkFile))
                  {
                      lines = File.ReadAllLines(checkFile);
                      string oldsha1;
                      lineIndex = GetCheckFileLineIndexSha1(lines, name, out oldsha1);
                  }
                  if (lineIndex == -1)
                  {
                      if (lines.Length == 0 || lines[lines.Length - 1].Trim().Length > 0)
                      {
                          Array.Resize(ref lines, lines.Length + 1);
                      }
                      lineIndex = lines.Length - 1;

                      // End with new line
                      Array.Resize(ref lines, lines.Length + 1);
                  }
                  lines[lineIndex] = newLine;
                  File.WriteAllLines(checkFile, lines);
              }
              catch (Exception ex)
              {
                  Log.LogWarningFromException(ex, showStackTrace: false);
              }
          }

          private int GetCheckFileLineIndexSha1(string[] lines, string name, out string sha1)
          {
              for (int i = 0; i < lines.Length; i++)
              {
                  string line = lines[i];
                  string[] lineParts = line.Split('=');
                  if (lineParts.Length == 2)
                  {
                      string lineTag = lineParts[0].Trim();
                      string lineSha1 = lineParts[1].Trim();
                      if (lineTag == name)
                      {
                          sha1 = lineSha1;
                          return i;
                      }
                  }
              }
              sha1 = null;
              return -1;
          }

          private bool CheckFileSha1(string file, string expectedSha1, out string actualSha1)
          {
              using (var fs = new FileStream(file, FileMode.Open))
              {
                  var hasher = System.Security.Cryptography.SHA1.Create();
                  byte[] hash = hasher.ComputeHash(fs);
                  actualSha1 = BytesToHexString(hash);
                  if (String.Equals(actualSha1, expectedSha1, StringComparison.OrdinalIgnoreCase))
                  {
                      return true;
                  }
              }
              return false;
          }

          private string BytesToHexString(byte[] data)
          {
              var sb = new StringBuilder();
              foreach (byte b in data)
              {
                  sb.Append(b.ToString("x2"));
              }
              return sb.ToString();
          }

          private static void ExtractZip(string zipPath, string destinationDirectory, bool overwrite)
          {
              var archive = ZipFile.OpenRead(zipPath);
              if (!overwrite)
              {
                  archive.ExtractToDirectory(destinationDirectory);
                  return;
              }
              foreach (ZipArchiveEntry file in archive.Entries)
              {
                  string fileName = Path.Combine(destinationDirectory, file.FullName);
                  string directory = Path.GetDirectoryName(fileName);
                  if (!Directory.Exists(directory))
                  {
                      Directory.CreateDirectory(directory);
                  }
                  if (file.Name != String.Empty)
                  {
                      file.ExtractToFile(fileName, true);
                  }
              }
          }
        ]]>
      </Code>
    </Task>
  </UsingTask>
</Project>
