param(
    [Parameter(Mandatory = $true)]
    [string]$InputDirectory,

    [string]$OutputFile = "",

    [string]$Pattern = "*-compare.csv",

    [string]$CellMedianFile = ""
)

$ErrorActionPreference = 'Stop'
$root = (Resolve-Path -LiteralPath $InputDirectory).Path
if (-not $OutputFile) {
    $OutputFile = Join-Path $root 'aggregate.csv'
}
if (-not $CellMedianFile) {
    $CellMedianFile = Join-Path $root 'cell-medians.csv'
}

$files = @(Get-ChildItem -LiteralPath $root -Filter $Pattern -File)
if ($files.Count -eq 0) {
    throw "No $Pattern files found in $root"
}

$rows = @($files | ForEach-Object { Import-Csv -LiteralPath $_.FullName }) |
    Where-Object { $_.status -eq 'ok' }

function Get-Median([double[]]$Values) {
    if ($Values.Count -eq 0) { return [double]::NaN }
    $ordered = @($Values | Sort-Object)
    $middle = [int]($ordered.Count / 2)
    if (($ordered.Count % 2) -eq 0) {
        return ($ordered[$middle - 1] + $ordered[$middle]) / 2.0
    }
    return $ordered[$middle]
}

function Get-GeometricMean([double[]]$Values) {
    $positive = @($Values | Where-Object { $_ -gt 0 -and -not [double]::IsNaN($_) })
    if ($positive.Count -eq 0) { return [double]::NaN }
    $logSum = 0.0
    foreach ($value in $positive) { $logSum += [math]::Log($value) }
    return [math]::Exp($logSum / $positive.Count)
}

function Get-Mode($Values) {
    $groups = @($Values | Group-Object | Sort-Object Count -Descending)
    if ($groups.Count -eq 0) { return '' }
    return $groups[0].Name
}

# Every implementation must produce the same observable result for an
# identical trace.  A mismatch invalidates the performance run.
$mismatches = @($rows |
    Group-Object profile,type,workload,n,operations,repeat,seed |
    Where-Object { @($_.Group.checksum | Select-Object -Unique).Count -ne 1 })
if ($mismatches.Count -ne 0) {
    throw "Checksum mismatch in $($mismatches.Count) benchmark cell(s)"
}

$medians = @($rows |
    Group-Object profile,type,structure,workload,n,operations,leaf,fanout,levels |
    ForEach-Object {
        $group = $_.Group
        $candidate = $group[0].structure
        if ($candidate -like 'adaptive_*' -or $candidate -like 'tiered_forced_*') {
            $candidate += "@initial_$($group[0].leaf)_$($group[0].fanout)_$($group[0].levels)"
        }
        [pscustomobject]@{
            profile = $group[0].profile
            type = $group[0].type
            structure = $candidate
            workload = $group[0].workload
            n = [uint64]$group[0].n
            operations = [uint64]$group[0].operations
            leaf = [uint64]$group[0].leaf
            fanout = [uint64]$group[0].fanout
            levels = [uint64]$group[0].levels
            final_leaf = Get-Mode @($group.final_leaf)
            final_fanout = Get-Mode @($group.final_fanout)
            final_levels = Get-Mode @($group.final_levels)
            final_mode = Get-Mode @($group.final_mode)
            median_construction_ns = Get-Median @($group.construction_ns | ForEach-Object { [double]$_ })
            median_initial_bytes = Get-Median @($group.initial_bytes | ForEach-Object { [double]$_ })
            median_ns = Get-Median @($group.total_ns | ForEach-Object { [double]$_ })
            median_ns_per_op = Get-Median @($group.ns_per_op | ForEach-Object { [double]$_ })
            median_bytes = Get-Median @($group.allocated_bytes | ForEach-Object { [double]$_ })
            median_p99_ns = Get-Median @($group.p99_ns | ForEach-Object { [double]$_ })
            median_max_maintenance_ns = Get-Median @($group.max_maintenance_ns | ForEach-Object { [double]$_ })
            median_max_transition_ns = Get-Median @($group.max_transition_ns | ForEach-Object { [double]$_ })
            median_switches = Get-Median @($group.switches | ForEach-Object { [double]$_ })
            median_shape_rebuilds = Get-Median @($group.shape_rebuilds | ForEach-Object { [double]$_ })
            median_leaf_rebuilds = Get-Median @($group.leaf_rebuilds | ForEach-Object { [double]$_ })
            median_directory_rebuilds = Get-Median @($group.directory_rebuilds | ForEach-Object { [double]$_ })
            median_policy_evaluations = Get-Median @($group.policy_evaluations | ForEach-Object { [double]$_ })
            median_last_forecast_operations = Get-Median @($group.last_forecast_operations | ForEach-Object { [double]$_ })
            median_last_evidence_windows = Get-Median @($group.last_evidence_windows | ForEach-Object { [double]$_ })
            median_last_vector_cost = Get-Median @($group.last_vector_cost | ForEach-Object { [double]$_ })
            median_last_tiered_cost = Get-Median @($group.last_tiered_cost | ForEach-Object { [double]$_ })
            median_last_current_cost = Get-Median @($group.last_current_cost | ForEach-Object { [double]$_ })
            median_last_conversion_cost = Get-Median @($group.last_conversion_cost | ForEach-Object { [double]$_ })
            median_last_expected_saving = Get-Median @($group.last_expected_saving | ForEach-Object { [double]$_ })
            median_last_local_edit_fraction = Get-Median @($group.last_local_edit_fraction | ForEach-Object { [double]$_ })
            median_last_edit_fraction = Get-Median @($group.last_edit_fraction | ForEach-Object { [double]$_ })
        }
    })

$medians | Sort-Object profile,type,workload,n,operations,structure |
    Export-Csv -LiteralPath $CellMedianFile -NoTypeInformation -Encoding utf8

$fixedNames = @('std::vector_reserved', 'std::deque',
                'std::list_index_semantics')
$bestFixed = @{}
$medians | Group-Object profile,type,workload,n,operations | ForEach-Object {
    $eligible = @($_.Group | Where-Object {
        $fixedNames -contains $_.structure -or $_.structure -like 'tiered_forced_leaf_*'
    })
    if ($eligible.Count -gt 0) {
        $key = "$($_.Group[0].profile)|$($_.Group[0].type)|$($_.Group[0].workload)|$($_.Group[0].n)|$($_.Group[0].operations)"
        $bestFixed[$key] = ($eligible.median_ns | Measure-Object -Minimum).Minimum
    }
}

$cellScores = @($medians | ForEach-Object {
    $key = "$($_.profile)|$($_.type)|$($_.workload)|$($_.n)|$($_.operations)"
    if ($bestFixed.ContainsKey($key) -and $_.median_ns -gt 0) {
        [pscustomobject]@{
            profile = $_.profile
            structure = $_.structure
            type = $_.type
            workload = $_.workload
            n = $_.n
            operations = $_.operations
            score = [double]$bestFixed[$key] / [double]$_.median_ns
        }
    }
})

# Hierarchical aggregation prevents a family with more sizes from receiving a
# larger implicit weight: sizes -> workloads -> types -> compiler profiles.
$byWorkload = @($cellScores | Group-Object profile,structure,type,workload |
    ForEach-Object {
        $g = $_.Group
        [pscustomobject]@{
            profile = $g[0].profile; structure = $g[0].structure
            type = $g[0].type; workload = $g[0].workload
            score = Get-GeometricMean @($g.score)
        }
    })
$byType = @($byWorkload | Group-Object profile,structure,type |
    ForEach-Object {
        $g = $_.Group
        [pscustomobject]@{
            profile = $g[0].profile; structure = $g[0].structure
            type = $g[0].type; score = Get-GeometricMean @($g.score)
        }
    })
$byProfile = @($byType | Group-Object profile,structure |
    ForEach-Object {
        $g = $_.Group
        [pscustomobject]@{
            profile = $g[0].profile
            structure = $g[0].structure
            score = Get-GeometricMean @($g.score)
            types = $g.Count
            cells = @($cellScores | Where-Object {
                $_.profile -eq $g[0].profile -and $_.structure -eq $g[0].structure
            }).Count
        }
    })
$overall = @($byProfile | Group-Object structure | ForEach-Object {
    $g = $_.Group
    [pscustomobject]@{
        profile = 'ALL'
        structure = $g[0].structure
        score = Get-GeometricMean @($g.score)
        types = ($g.types | Measure-Object -Maximum).Maximum
        cells = ($g.cells | Measure-Object -Sum).Sum
    }
})

@($byProfile + $overall) | Sort-Object profile,@{Expression='score';Descending=$true} |
    Export-Csv -LiteralPath $OutputFile -NoTypeInformation -Encoding utf8

Write-Host "Validated $($rows.Count) raw rows from $($files.Count) profile file(s)."
Write-Host "Cell medians: $CellMedianFile"
Write-Host "Aggregate:    $OutputFile"
@($overall | Sort-Object score -Descending) |
    Format-Table structure,score,types,cells -AutoSize
