#! /usr/bin/env pwsh

[CmdletBinding()]
Param(
    [String]$VcpkgRoot = ''
)

if ([String]::IsNullOrEmpty($VcpkgRoot)) {
    $VcpkgRoot = "${PSScriptRoot}/.."
}

$VcpkgRoot = Resolve-Path $VcpkgRoot

if (-not (Test-Path "$VcpkgRoot/.vcpkg-root")) {
    throw "Invalid vcpkg instance, did you forget -VcpkgRoot?"
}

class CMakeDocumentation {
    [String]$Filename
    [String[]]$ActualDocumentation
    [Bool]$IsDeprecated
    [String]$DeprecationMessage
    [String]$DeprecatedByName
    [String]$DeprecatedByPath
    [Bool]$HasError
}

[String[]]$cmakeScriptsPorts = @(
    'vcpkg-cmake'
    'vcpkg-cmake-config'
    'vcpkg-pkgconfig-get-modules'
)

[CMakeDocumentation[]]$tableOfContents = @()
[CMakeDocumentation[]]$internalTableOfContents = @()
$portTableOfContents = [ordered]@{}

function RelativeUnixPathTo
{
    Param(
        [Parameter(Mandatory)]
        [String]$Path,
        [Parameter(Mandatory)]
        [String]$Base
    )

    $Path = Resolve-Path -LiteralPath $Path
    $Base = Resolve-Path -LiteralPath $Base

    if ($IsWindows)
    {
        if ((Split-Path -Qualifier $Path) -ne (Split-Path -Qualifier $Base))
        {
            throw "It is not possible to get the relative unix path from $Base to $Path"
        }
    }

    $Path = $Path -replace '\\','/'
    $Base = $Base -replace '\\','/'

    [String[]]$PathArray = $Path -split '/'
    [String[]]$BaseArray = $Base -split '/'

    [String[]]$Result = @()

    $Idx = 0

    while ($Idx -lt $PathArray.Length -and $Idx -lt $BaseArray.Length)
    {
        if ($PathArray[$Idx] -ne $BaseArray[$Idx])
        {
            break
        }
        ++$Idx
    }

    for ($BaseIdx = $Idx; $BaseIdx -lt $BaseArray.Length; ++$BaseIdx)
    {
        $Result += '..'
    }
    for ($PathIdx = $Idx; $PathIdx -lt $PathArray.Length; ++$PathIdx)
    {
        $Result += $PathArray[$PathIdx]
    }

    $Result -join '/'
}
function WriteFile
{
    Param(
        [String[]]$Value,
        [String]$Path
    )
    # note that we use this method of getting the utf-8 bytes in order to:
    #  - have no final `r`n, which happens when Set-Content does the thing automatically on Windows
    #  - have no BOM, which happens when one uses [System.Text.Encoding]::UTF8
    [byte[]]$ValueAsBytes = (New-Object -TypeName 'System.Text.UTF8Encoding').GetBytes($Value -join "`n")
    Set-Content -Path $Path -Value $ValueAsBytes -AsByteStream
}
function FinalDocFile
{
    Param(
        [CMakeDocumentation]$Docs,
        [String]$PathToFile # something like docs/maintainers/blah.md
    )
    [String[]]$documentation = @()

    if ($Docs.ActualDocumentation.Length -eq 0)
    {
        throw "Invalid documentation: empty docs"
    }

    $documentation += $Docs.ActualDocumentation[0] # name line
    if ($Docs.IsDeprecated)
    {
        if ($null -eq $Docs.DeprecationMessage -or $Docs.DeprecationMessage -match '^ *$')
        {
            if(![string]::IsNullOrEmpty($Docs.DeprecatedByName))
            {
                $message = " in favor of [``$($Docs.DeprecatedByName)``]($($Docs.DeprecatedByPath)$($Docs.DeprecatedByName).md)"
                $Docs.DeprecatedByPath -match '^ports/([a-z\-]+)/$' | Out-Null
                $port = $matches[1]
                if(![string]::IsNullOrEmpty($port))
                {
                    $message += " from the $port port."
                }
            }
            $documentation += @("", "**This function has been deprecated$message**")
        }
        else
        {
            $documentation += @("", "**This function has been deprecated $($Docs.DeprecationMessage)**")
        }
    }
    $documentation += @("", "The latest version of this document lives in the [vcpkg repo](https://github.com/Microsoft/vcpkg/blob/master/docs/$PathToFile).")

    $documentation += $Docs.ActualDocumentation[1..$Docs.ActualDocumentation.Length]

    $relativePath = RelativeUnixPathTo $Docs.Filename $VcpkgRoot
    $documentation += @(
        "",
        "## Source",
        "[$($relativePath -replace '_','\_')](https://github.com/Microsoft/vcpkg/blob/master/$relativePath)",
        ""
    )

    $documentation
}

function ParseCmakeDocComment
{
    Param(
        [Parameter(Mandatory)]
        [System.IO.FileSystemInfo]$Filename
    )

    $Docs = New-Object 'CMakeDocumentation'
    $Docs.HasError = $False
    $Docs.IsDeprecated = $False
    $Docs.Filename = $Filename.FullName

    [String[]]$contents = Get-Content $Filename

    if ($contents[0] -eq '# DEPRECATED')
    {
        $Docs.IsDeprecated = $True
    }
    elseif($contents[0] -match '^# DEPRECATED( BY (([^/]+/)+)(.+))?((: *)(.*))?$')
    {
        $Docs.IsDeprecated = $True
        $Docs.DeprecatedByPath = $matches[2]
        $Docs.DeprecatedByName = $matches[4]
        $Docs.DeprecationMessage = $matches[7]
    }

    [String]$startCommentRegex = '#\[(=*)\['
    [String]$endCommentRegex = ''
    [Bool]$inComment = $False

    $contents = $contents | ForEach-Object {
        if (-not $inComment) {
            if ($_ -match "^\s*${startCommentRegex}(\.[a-z]*)?:?\s*$") {
                if (-not [String]::IsNullOrEmpty($matches[2]) -and $matches[2] -ne '.md') {
                    Write-Warning "The documentation in $($Filename.FullName) doesn't seem to be markdown (extension: $($matches[2])). Only markdown is supported; please rewrite the documentation in markdown."
                }
                $inComment = $True
                $endCommentRegex = "\]$($matches[1])\]"
            } elseif ($_ -match $startCommentRegex) {
                $Docs.HasError = $True
                Write-Warning "Invalid start of comment -- the comment start must be at the beginning of the line.
    (on line: `"$_`")"
            } else {
                # do nothing -- we're outside a comment, so cmake code
            }
        } else {
            if ($_ -match "^\s*#?${endCommentRegex}\s*$") {
                $inComment = $False
                $endCommentRegex = ''
            } elseif ($_ -match $endCommentRegex) {
                $Docs.HasError = $True
                Write-Warning "Invalid end of comment -- the comment end must be on it's own on a line.
    (on line: `"$_`")"
            } else {
                # regular documentation line
                $_
            }
        }
    }

    if ($inComment) {
        Write-Warning "File $($Filename.FullName) has an unclosed comment."
        $Docs.HasError = $True
    }

    if (-not [String]::IsNullOrEmpty($contents))
    {
        $Docs.ActualDocumentation = $contents
    }

    $Docs
}

Get-ChildItem "$VcpkgRoot/scripts/cmake" -Filter '*.cmake' | ForEach-Object {
    $docs = ParseCmakeDocComment $_
    [Bool]$isInternalFunction = $_.Name.StartsWith("z_vcpkg")

    if ($docs.IsDeprecated -and $null -eq $docs.ActualDocumentation)
    {
        return
    }
    if ($docs.HasError)
    {
        return
    }

    if ($null -ne $docs.ActualDocumentation)
    {
        if ($isInternalFunction)
        {
            $pathToFile = "maintainers/internal/$($_.BaseName).md"
            WriteFile `
                -Path "$PSScriptRoot/$pathToFile" `
                -Value (FinalDocFile $docs)

            $internalTableOfContents += $docs
        }
        else
        {
            $pathToFile = "maintainers/$($_.BaseName).md"
            WriteFile `
                -Path "$PSScriptRoot/$pathToFile" `
                -Value (FinalDocFile $docs $pathToFile)

            $tableOfContents += $docs
        }
    }
    elseif (-not $isInternalFunction)
    {
        # don't worry about undocumented internal functions
        Write-Warning "The cmake function in file $($_.FullName) doesn't seem to have any documentation. Make sure the documentation comments are correctly written."
    }
}

$cmakeScriptsPorts | ForEach-Object {
    $portName = $_

    Copy-Item "$VcpkgRoot/ports/$portName/README.md" "$PSScriptRoot/maintainers/ports/$portName.md"
    New-Item -Path "$PSScriptRoot/maintainers/ports/$portName" -Force -ItemType 'Directory' | Out-Null

    $portTableOfContents[$portName] = @()

    Get-ChildItem "$VcpkgRoot/ports/$portName" -Filter '*.cmake' | ForEach-Object {
        if ($_.Name -eq 'vcpkg-port-config.cmake' -or $_.Name -eq 'portfile.cmake')
        {
            return
        }

        $docs = ParseCmakeDocComment $_

        if ($docs.IsDeprecated -and $null -eq $docs.ActualDocumentation)
        {
            return
        }
        if ($docs.HasError)
        {
            return
        }

        if ($null -ne $docs.ActualDocumentation)
        {
            $pathToFile = "maintainers/ports/$portName/$($_.BaseName).md"
            WriteFile `
                -Path "$PSScriptRoot/$pathToFile" `
                -Value (FinalDocFile $docs $pathToFile)
            $portTableOfContents[$portName] += $docs
        }
        else
        {
            Write-Warning "The cmake function in file $($_.FullName) doesn't seem to have any documentation. Make sure the documentation comments are correctly written."
        }
    }
}

$portfileFunctionsContent = @(
    '<!-- Run regenerate.ps1 to extract scripts documentation -->',
    '',
    '# Portfile helper functions')

function GetDeprecationMessage
{
    Param(
        [CMakeDocumentation]$Doc
    )
    $message = ''
    if ($Doc.IsDeprecated)
    {
        $message = " (deprecated"
        if(![string]::IsNullOrEmpty($Doc.DeprecatedByName))
        {
            $message += ", use [$($($Doc.DeprecatedByName) -replace '_','\_')]($($Doc.DeprecatedByPath)$($Doc.DeprecatedByName).md)"
        }
        $message += ")"
    }
    $message
}

$DocsName = @{ expression = { Split-Path -LeafBase $_.Filename } }
$tableOfContents | Sort-Object -Property $DocsName -Culture '' | ForEach-Object {
    $name = Split-Path -LeafBase $_.Filename
    $portfileFunctionsContent += "- [$($name -replace '_','\_')]($name.md)" + $(GetDeprecationMessage $_)
}
$portfileFunctionsContent += @("", "## Internal Functions", "")
$internalTableOfContents | Sort-Object -Property $DocsName -Culture '' | ForEach-Object {
    $name = Split-Path -LeafBase $_.Filename
    $portfileFunctionsContent += "- [$($name -replace '_','\_')](internal/$name.md)" + $(GetDeprecationMessage $_)
}

$portfileFunctionsContent += @("", "## Scripts from Ports")
$portTableOfContents.GetEnumerator() | ForEach-Object {
    $portName = $_.Name
    $cmakeDocs = $_.Value
    $portfileFunctionsContent += @("", "### [$portName](ports/$portName.md)", "")
    $cmakeDocs | ForEach-Object {
        $name = Split-Path -LeafBase $_.Filename
        $portfileFunctionsContent += "- [$($name -replace '_','\_')](ports/$portName/$name.md)" + $(GetDeprecationMessage $_)
    }
}

$portfileFunctionsContent += "" # final newline

WriteFile `
    -Path "$PSScriptRoot/maintainers/portfile-functions.md" `
    -Value $portfileFunctionsContent
