# cSpell:ignore prenumsep
# cSpell:ignore pbeta

<#
.DESCRIPTION
Parses a semver version string into its components and supports operations around it that we use for versioning our packages.

See https://azure.github.io/azure-sdk/policies_releases.html#package-versioning

Example: 1.2.3-beta.4
Components: Major.Minor.Patch-PrereleaseLabel.PrereleaseNumber

Example: 1.2.3-alpha.20200828.4
Components: Major.Minor.Patch-PrereleaseLabel.PrereleaseNumber.BuildNumber

Note: A builtin Powershell version of SemVer exists in 'System.Management.Automation'. At this time, it does not parsing of PrereleaseNumber. It's name is also type accelerated to 'SemVer'.
#>

class AzureEngSemanticVersion : IComparable {
  [int] $Major
  [int] $Minor
  [int] $Patch
  [string] $PrereleaseLabelSeparator
  [string] $PrereleaseLabel
  [string] $PrereleaseNumberSeparator
  [string] $BuildNumberSeparator
  # BuildNumber is string to preserve zero-padding where applicable
  [string] $BuildNumber
  [int] $PrereleaseNumber
  [bool] $IsPrerelease
  [string] $VersionType
  [string] $RawVersion
  [bool] $IsSemVerFormat
  [string] $DefaultPrereleaseLabel
  [string] $DefaultAlphaReleaseLabel

  # Regex inspired but simplified from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
  # Validation: https://regex101.com/r/vkijKf/426
  static [string] $SEMVER_REGEX = "(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:(?<presep>-?)(?<prelabel>[a-zA-Z]+)(?:(?<prenumsep>\.?)(?<prenumber>[0-9]{1,8})(?:(?<buildnumsep>\.?)(?<buildnumber>\d{1,3}))?)?)?"

  static [AzureEngSemanticVersion] ParseVersionString([string] $versionString)
  {
    $version = [AzureEngSemanticVersion]::new($versionString)

    if (!$version.IsSemVerFormat) {
      return $null
    }
    return $version
  }

  static [AzureEngSemanticVersion] ParsePythonVersionString([string] $versionString)
  {
    $version = [AzureEngSemanticVersion]::ParseVersionString($versionString)

    if (!$version) {
      return $null
    }

    $version.SetupPythonConventions()
    return $version
  }
  
  AzureEngSemanticVersion([string] $versionString)
  {
    if ($versionString -match "^$([AzureEngSemanticVersion]::SEMVER_REGEX)$")
    {
      $this.IsSemVerFormat = $true
      $this.RawVersion = $versionString
      $this.Major = [int]$matches.Major
      $this.Minor = [int]$matches.Minor
      $this.Patch = [int]$matches.Patch

      # If Language exists and is set to python setup the python conventions.
      $parseLanguage = (Get-Variable -Name "Language" -ValueOnly -ErrorAction "Ignore")
      if ($parseLanguage -eq "python") {
        $this.SetupPythonConventions()
      }
      else {
        $this.SetupDefaultConventions()
      }

      if ($null -eq $matches['prelabel'])
      {
        # artificially provide these values for non-prereleases to enable easy sorting of them later than prereleases.
        $this.PrereleaseLabel = "zzz"
        $this.PrereleaseNumber = 99999999
        $this.IsPrerelease = $false
        $this.VersionType = "GA"
        if ($this.Major -eq 0) {
           # Treat initial 0 versions as a prerelease beta's
          $this.VersionType = "Beta"
          $this.IsPrerelease = $true
        }
        elseif ($this.Patch -ne 0) {
          $this.VersionType = "Patch"
        }
      }
      else
      {
        $this.PrereleaseLabel = $matches["prelabel"]
        $this.PrereleaseLabelSeparator = $matches["presep"]
        $this.PrereleaseNumber = [int]$matches["prenumber"]
        $this.PrereleaseNumberSeparator = $matches["prenumsep"]
        $this.IsPrerelease = $true
        $this.VersionType = "Beta"

        $this.BuildNumberSeparator = $matches["buildnumsep"]
        $this.BuildNumber = $matches["buildnumber"]
      }
    }
    else
    {
      $this.RawVersion = $versionString
      $this.IsSemVerFormat = $false
    }
  }

  # If a prerelease label exists, it must be 'beta', and similar semantics used in our release guidelines
  # See https://azure.github.io/azure-sdk/policies_releases.html#package-versioning
  [bool] HasValidPrereleaseLabel()
  {
    if ($this.IsPrerelease -eq $true) {
      if ($this.PrereleaseLabel -ne $this.DefaultPrereleaseLabel -and $this.PrereleaseLabel -ne $this.DefaultAlphaReleaseLabel) {
        Write-Host "Unexpected pre-release identifier '$($this.PrereleaseLabel)', "`
                   "should be '$($this.DefaultPrereleaseLabel)' or '$($this.DefaultAlphaReleaseLabel)'"
        return $false;
      }
      if ($this.PrereleaseNumber -lt 1)
      {
        Write-Host "Unexpected pre-release version '$($this.PrereleaseNumber)', should be >= '1'"
        return $false;
      }
    }

    return $true;
  }

  [string] ToString()
  {
    $versionString = "{0}.{1}.{2}" -F $this.Major, $this.Minor, $this.Patch

    if ($this.IsPrerelease -and $this.PrereleaseLabel -ne "zzz")
    {
      $versionString += $this.PrereleaseLabelSeparator + $this.PrereleaseLabel + `
                        $this.PrereleaseNumberSeparator + $this.PrereleaseNumber
      if ($this.BuildNumber) {
          $versionString += $this.BuildNumberSeparator + $this.BuildNumber
      }
    }
    return $versionString;
  }

  [void] IncrementAndSetToPrerelease() {
    if ($this.IsPrerelease -eq $false)
    {
      $this.PrereleaseLabel = $this.DefaultPrereleaseLabel
      $this.PrereleaseNumber = 1
      $this.Minor++
      $this.Patch = 0
      $this.IsPrerelease = $true
    }
    else
    {
      if ($this.BuildNumber) {
        throw "Cannot increment releases tagged with azure pipelines build numbers"
      }
      $this.PrereleaseNumber++
    }
  }

  [void] SetupPythonConventions()
  {
    # Python uses no separators and "b" for beta so this sets up the the object to work with those conventions
    $this.PrereleaseLabelSeparator = $this.PrereleaseNumberSeparator = $this.BuildNumberSeparator = ""
    $this.DefaultPrereleaseLabel = "b"
    $this.DefaultAlphaReleaseLabel = "a"
  }

  [void] SetupDefaultConventions()
  {
    # Use the default common conventions
    $this.PrereleaseLabelSeparator = "-"
    $this.PrereleaseNumberSeparator = "."
    $this.BuildNumberSeparator = "."
    $this.DefaultPrereleaseLabel = "beta"
    $this.DefaultAlphaReleaseLabel = "alpha"
  }

  [int] CompareTo($other)
  {
    if ($other -isnot [AzureEngSemanticVersion]) {
      throw "Cannot compare $other with $this"
    }

    $ret = $this.Major.CompareTo($other.Major)
    if ($ret) { return $ret }

    $ret = $this.Minor.CompareTo($other.Minor)
    if ($ret) { return $ret }

    $ret = $this.Patch.CompareTo($other.Patch)
    if ($ret) { return $ret }

    # Mimic PowerShell that uses case-insensitive comparisons by default.
    $ret = [string]::Compare($this.PrereleaseLabel, $other.PrereleaseLabel, $true)
    if ($ret) { return $ret }

    $ret = $this.PrereleaseNumber.CompareTo($other.PrereleaseNumber)
    if ($ret) { return $ret }

    return ([int] $this.BuildNumber).CompareTo([int] $other.BuildNumber)
  }

  static [string[]] SortVersionStrings([string[]] $versionStrings)
  {
    $versions = $versionStrings | ForEach-Object { [AzureEngSemanticVersion]::ParseVersionString($_) }
    $sortedVersions = [AzureEngSemanticVersion]::SortVersions($versions)
    return ($sortedVersions | ForEach-Object { $_.RawVersion })
  }

  static [AzureEngSemanticVersion[]] SortVersions([AzureEngSemanticVersion[]] $versions)
  {
    return $versions | Sort-Object -Descending
  }

  static [void] QuickTests()
  {
    $global:Language = ""
    $versions = @(
      "1.0.1",
      "2.0.0",
      "2.0.0-alpha.20200920",
      "2.0.0-alpha.20200920.1",
      "2.0.0-beta.2",
      "1.0.10",
      "2.0.0-alpha.20201221.03",
      "2.0.0-alpha.20201221.1",
      "2.0.0-alpha.20201221.5",
      "2.0.0-alpha.20201221.2",
      "2.0.0-alpha.20201221.10",
      "2.0.0-beta.1",
      "2.0.0-beta.10",
      "1.0.0",
      "1.0.0b2",
      "1.0.2")

    $expectedSort = @(
      "2.0.0",
      "2.0.0-beta.10",
      "2.0.0-beta.2",
      "2.0.0-beta.1",
      "2.0.0-alpha.20201221.10",
      "2.0.0-alpha.20201221.5",
      "2.0.0-alpha.20201221.03",
      "2.0.0-alpha.20201221.2",
      "2.0.0-alpha.20201221.1",
      "2.0.0-alpha.20200920.1",
      "2.0.0-alpha.20200920",
      "1.0.10",
      "1.0.2",
      "1.0.1",
      "1.0.0",
      "1.0.0b2")

    $sort = [AzureEngSemanticVersion]::SortVersionStrings($versions)

    for ($i = 0; $i -lt $expectedSort.Count; $i++)
    {
      if ($sort[$i] -ne $expectedSort[$i]) {
        Write-Host "Error: Incorrect version sort:"
        Write-Host "Expected: "
        Write-Host $expectedSort
        Write-Host "Actual:"
        Write-Host $sort
        break
      }
    }

    $alphaVerString = "1.2.3-alpha.20200828.9"
    $alphaVer = [AzureEngSemanticVersion]::new($alphaVerString)
    if (!$alphaVer.IsPrerelease) {
      Write-Host "Expected alpha version to be marked as prerelease"
    }
    if ($alphaVer.Major -ne 1 -or $alphaVer.Minor -ne 2 -or $alphaVer.Patch -ne 3 -or `
        $alphaVer.PrereleaseLabel -ne "alpha" -or $alphaVer.PrereleaseNumber -ne 20200828 -or $alphaVer.BuildNumber -ne 9) {
      Write-Host "Error: Didn't correctly parse alpha version string $alphaVerString"
    }
    if ($alphaVerString -ne $alphaVer.ToString()) {
      Write-Host "Error: alpha string did not correctly round trip with ToString. Expected: $($alphaVerString), Actual: $($alphaVer)"
    }

    $global:Language = "python"
    $pythonAlphaVerString = "1.2.3a20200828009"
    $pythonAlphaVer = [AzureEngSemanticVersion]::new($pythonAlphaVerString)
    if (!$pythonAlphaVer.IsPrerelease) {
      Write-Host "Expected python alpha version to be marked as prerelease"
    }
    # Note: For python we lump build number into prerelease number, since it simplifies the code and regex, and is behaviorally the same
    if ($pythonAlphaVer.Major -ne 1 -or $pythonAlphaVer.Minor -ne 2 -or $pythonAlphaVer.Patch -ne 3 `
        -or $pythonAlphaVer.PrereleaseLabel -ne "a" -or $pythonAlphaVer.PrereleaseNumber -ne 20200828 `
        -or $pythonAlphaVer.BuildNumber -ne "009") {
      Write-Host "Error: Didn't correctly parse python alpha version string $pythonAlphaVerString"
    }
    if ($pythonAlphaVerString -ne $pythonAlphaVer.ToString()) {
      Write-Host "Error: python alpha string did not correctly round trip with ToString. Expected: $($pythonAlphaVerString), Actual: $($pythonAlphaVer)"
    }

    $versions = @("1.0.1", "2.0.0", "2.0.0a20201208001", "2.0.0a20201105020", "2.0.0a20201208012", `
                  "2.0.0b2", "1.0.10", "2.0.0b1", "2.0.0b10", "1.0.0", "1.0.0b2", "1.0.2")
    $expectedSort = @("2.0.0", "2.0.0b10", "2.0.0b2", "2.0.0b1", "2.0.0a20201208012", "2.0.0a20201208001", `
                      "2.0.0a20201105020", "1.0.10", "1.0.2", "1.0.1", "1.0.0", "1.0.0b2")
    $sort = [AzureEngSemanticVersion]::SortVersionStrings($versions)
    for ($i = 0; $i -lt $expectedSort.Count; $i++)
    {
      if ($sort[$i] -ne $expectedSort[$i]) { 
        Write-Host "Error: Incorrect python version sort:"
        Write-Host "Expected: "
        Write-Host $expectedSort
        Write-Host "Actual:"
        Write-Host $sort
        break
      }
    }

    $global:Language = ""

    $gaVerString = "1.2.3"
    $gaVer = [AzureEngSemanticVersion]::ParseVersionString($gaVerString)
    if ($gaVer.Major -ne 1 -or $gaVer.Minor -ne 2 -or $gaVer.Patch -ne 3) {
      Write-Host "Error: Didn't correctly parse ga version string $gaVerString"
    }
    if ($gaVerString -ne $gaVer.ToString()) {
      Write-Host "Error: Ga string did not correctly round trip with ToString. Expected: $($gaVerString), Actual: $($gaVer)"
    }
    $gaVer.IncrementAndSetToPrerelease()
    if ("1.3.0-beta.1" -ne $gaVer.ToString()) {
      Write-Host "Error: Ga string did not correctly increment"
    }

    $betaVerString = "1.2.3-beta.4"
    $betaVer = [AzureEngSemanticVersion]::ParseVersionString($betaVerString)
    if ($betaVer.Major -ne 1 -or $betaVer.Minor -ne 2 -or $betaVer.Patch -ne 3 -or $betaVer.PrereleaseLabel -ne "beta" -or $betaVer.PrereleaseNumber -ne 4) {
      Write-Host "Error: Didn't correctly parse beta version string $betaVerString"
    }
    if ($betaVerString -ne $betaVer.ToString()) {
      Write-Host "Error: beta string did not correctly round trip with ToString. Expected: $($betaVerString), Actual: $($betaVer)"
    }
    $betaVer.IncrementAndSetToPrerelease()
    if ("1.2.3-beta.5" -ne $betaVer.ToString()) {
      Write-Host "Error: Beta string did not correctly increment"
    }

    $pythonBetaVerString = "1.2.3b4"
    $pbetaVer = [AzureEngSemanticVersion]::ParsePythonVersionString($pythonBetaVerString)
    if ($pbetaVer.Major -ne 1 -or $pbetaVer.Minor -ne 2 -or $pbetaVer.Patch -ne 3 -or $pbetaVer.PrereleaseLabel -ne "b" -or $pbetaVer.PrereleaseNumber -ne 4) {
      Write-Host "Error: Didn't correctly parse python beta string $pythonBetaVerString"
    }
    if ($pythonBetaVerString -ne $pbetaVer.ToString()) {
      Write-Host "Error: python beta string did not correctly round trip with ToString"
    }
    $pbetaVer.IncrementAndSetToPrerelease()
    if ("1.2.3b5" -ne $pbetaVer.ToString()) {
      Write-Host "Error: Python beta string did not correctly increment"
    }

    Write-Host "QuickTests done"
  }
}
