param
(
    # web root directory for the AOS web application
    [Parameter(Mandatory=$true)]
    $webroot,
    # directory where the models are deployed
    [Parameter(Mandatory=$true)]
    $packagedir,
    # log file
    [Parameter(Mandatory=$true)]
    $log
)

$signature = @’
[DllImport("kernel32.dll")] 
public static extern bool CreateSymbolicLink(string symlinkFileName, string targetFileName, int flags);
‘@

$type = Add-Type -MemberDefinition $signature -Name Win32Utils -Namespace CreateSymbolicLink -PassThru

function InitializeLog
{
    if(Test-Path $Global:log){
        Write-Output "Deleting existing log file $Global:log."
        Remove-Item $Global:log -Force
    }

    New-Item -ItemType file $Global:log -Force
}

function Write-Log ([string]$message)
{
    $datetime=get-date -Format "MMddyyyyhhmmss"
    if($Global:log -ne $null){
        "$datetime`: $message" >> $Global:log
    }

    Write-Output "$datetime`: $message"
}

function CreateSymbolicLink([string]$webroot,[string]$packagedir)
{
    Write-Log "Starting the creation of symlinks."
    Write-Log "Dynamics 365 Unified Operations package directory is: $packagedir"
    Write-Log "AOS webroot is: $webroot"
    $appassembliesdir=Join-Path $webroot "bin\appassemblies"

    if(!(Test-Path $appassembliesdir)){
        Write-Log "Creating $appassembliesdir"
        New-Item -ItemType Directory -Path $appassembliesdir -Force
    }

    $packagesbindir=join-path $packagedir "bin"

    $directories=[System.IO.Directory]::EnumerateDirectories($packagedir,"*",[System.IO.SearchOption]::TopDirectoryOnly)
    foreach($moduledir in $directories)
    {
        if($moduledir -ne "$packagesbindir"){
            $bindir=Join-Path $moduledir "bin"
        }else{
            $bindir=$moduledir
        }

        if(Test-Path $bindir){
            Write-Log "Enumerating applicable files at '$bindir'."
            $files=New-Object 'System.Collections.Generic.List[System.String]'
            $files.AddRange([System.IO.Directory]::EnumerateFiles($bindir,"*.dll",[System.IO.SearchOption]::TopDirectoryOnly))
            $files.AddRange([System.IO.Directory]::EnumerateFiles($bindir,"*.netmodule",[System.IO.SearchOption]::TopDirectoryOnly))
            $files.AddRange([System.IO.Directory]::EnumerateFiles($bindir,"*.runtime",[System.IO.SearchOption]::TopDirectoryOnly))
            $files.AddRange([System.IO.Directory]::EnumerateFiles($bindir,"*.xml",[System.IO.SearchOption]::TopDirectoryOnly))

            foreach($filepath in $files){
               $file=[System.IO.Path]::GetFileName($filepath)
               $tmp1=Join-Path $webroot "bin\$file"
               $tmp2=Join-Path $appassembliesdir $file
               if(((Test-Path "$tmp1") -eq $false)){
                   if((Test-Path "$tmp2") -eq $true){
                        Write-Log "Removing existing symlink $tmp2"
                        Remove-Item $tmp2 -Force
                   }

                   Write-Log "Creating a symlink at: $tmp2 for source: $filepath"
                   if(-not $type::CreateSymbolicLink($tmp2,$filepath,0)){
                        throw "Symlink creation failed for file: $tmp2"
                   }
               }
            }
        }
    }

    Write-Log "Symlinks created."
}

function Run-Process($cmd, $arguments, [switch]$throwonerror){
    $process=New-Object System.Diagnostics.Process
    $process.StartInfo = New-Object System.Diagnostics.ProcessStartInfo
    $process.StartInfo.FileName=$cmd
    $process.StartInfo.Arguments=$arguments
    $process.StartInfo.UseShellExecute=$false
    $process.StartInfo.CreateNoWindow = $true
    $process.StartInfo.RedirectStandardError=$true
    $process.StartInfo.RedirectStandardOutput=$true
    Write-Log "Running: $cmd with arguments: $arguments"

    Write-Debug "Register ErrorDataReceived event on process"
    $action = { $errors += $Event.SourceEventArgs.Data }
    Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -Action $action | Out-Null
    $process.Start() | Out-Null
    $process.BeginErrorReadLine()  
    if ($process.StandardOutput -ne $null) { $ngenoutput = $process.StandardOutput.ReadToEnd() }
    $process.WaitForExit()
    Write-Log $ngenoutput
    Write-Log $errors
    if($throwonerror -and ($process.ExitCode -ne 0)){
        throw "$cmd failed."
    }
}

function UpdateProbingPath([string]$webroot){
    $webconfig=join-path $webroot "web.config"
    $appassembliesdir="bin\appassemblies"
    [System.Xml.XmlDocument] $xd=new-object System.Xml.XmlDocument
    $xd.Load($webconfig)
    $ns=New-Object System.Xml.XmlNamespaceManager($xd.NameTable)
    $ns.AddNamespace("ns",$xd.DocumentElement.NamespaceURI)

    # update probing path
    $tmp=join-path $webroot "bin\appassemblies"
    if(Test-Path $tmp){
        [string]$privatepath=$xd.Configuration.RunTime.AssemblyBinding.Probing.privatePath
        [string[]] $probingpaths=$privatepath.Split(";")
        if(!$probingpaths.Contains($appassembliesdir)){
            Write-Log "Adding $appassembliesdir to the private probing path in web.config"
            $privatepath += ";$appassembliesdir"
            $xd.Configuration.RunTime.AssemblyBinding.Probing.privatePath=$privatepath

            # save changes
            $xd.Save($webconfig)
        }
    }
}

function NgenFiles([string]$webroot,[switch]$updateprobingpath)
{
    Write-Log "Creating the native images for Dynamics assemblies."
    $appassembliesdir=Join-Path $webroot "bin\appassemblies"

    if(!(Test-Path "$appassembliesdir")){
        throw "bin\appassemblies (symlink to Dynamics assemblies) folder doesn't exist under the webroot."
    }
    
    $ngen=Join-Path ([System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory()) "ngen.exe"
    Write-Log "Ngen.exe path: $ngen"

    # ngen.exe needs an exe and config to generate native images
    $ngenexe=join-path $webroot "__ngen_exe.exe"
    $ngenexeconfig=join-path $webroot "__ngen_exe.exe.config"

    # delete existing files
    if(Test-Path $ngenexe){
        Write-Log "Removing existing $ngenexe"
        Remove-Item $ngenexe -Force
    }

    if(Test-Path $ngenexeconfig){
        Write-Log "Removing existing $ngenexeconfig"
        Remove-Item $ngenexeconfig -Force
    }

    if($updateprobingpath){
        UpdateProbingPath -webroot:$webroot
    }

    $webconfig=Join-Path $webroot "web.config"
    Write-Log "Copying $webconfig to $ngenexe"
    Copy-Item $webconfig $ngenexe

    Write-Log "Copying $webconfig to $ngenexeconfig"
    Copy-Item $webconfig $ngenexeconfig

    $dlls=New-Object 'System.Collections.Generic.List[System.String]'
    $dlls.AddRange([System.IO.Directory]::EnumerateFiles($appassembliesdir,"Dynamics.Ax.*.dll",[System.IO.SearchOption]::TopDirectoryOnly))

    # uninstall
    Write-Log "Uninstalling existing Dynamics native assembly images."
    foreach($dllpath in $dlls){
        $argument="uninstall `"$dllpath`" /nologo"
        Run-Process -cmd:$ngen -arguments:$argument # ngen uninstall throws error if assembly isn't registered which is expected; don't fail on it
    }

<#
    #install
    Write-Log "Creating native images for Dynamics assemblies..."
    foreach($dllpath in $dlls){
        $argument="install `"$dllpath`" /ExeConfig:`"$ngenexe`" /queue:1 /nologo /verbose"
        Run-Process -cmd:$ngen -arguments:$argument -throwonerror
    }

    Write-Log "Executing queued Ngen.exe jobs."
    $argument="executeQueuedItems /nologo /verbose"
    Run-Process -cmd:$ngen -arguments:$argument
    
    Write-Log "Native images for the Dynamics assemblies are created."
#>
}

function UpdateWebconfig([string]$webroot,[string]$uselazytypeloader)
{
   	Write-Log "Updating the AOS web configuration."
    $webconfig=join-path $webroot "web.config"
    $appassembliesdir="bin\appassemblies"
    [System.Xml.XmlDocument] $xd=new-object System.Xml.XmlDocument
    $xd.Load($webconfig)
    $ns=New-Object System.Xml.XmlNamespaceManager($xd.NameTable)
    $ns.AddNamespace("ns",$xd.DocumentElement.NamespaceURI)

    # add UseLazyTypeLoader setting
    $key="UseLazyTypeLoader"
    $value=$uselazytypeloader
    $existingNode = $xd.SelectSingleNode("//ns:add[@key='$key']",$ns)

    if($existingNode -ne $null){
        $existingValue=$existingNode.GetAttribute("value")
        if($existingValue -ne $value){ # update if the existing value -ne new value
    	    Write-Log "Updating configuration '$key'='$value'"
		    $existingNode.SetAttribute("value",$value)
            $xd.Save($webconfig) # save changes
        }
	}
    else{
		$addElement=$xd.CreateElement("add")
		$addElement.SetAttribute("key",$key)
		$addElement.SetAttribute("value",$value)
        $appSettings=$xd.SelectSingleNode("//ns:appSettings",$ns)
		Write-Log "Adding configuration '$key'='$value' to web.config"
		$appSettings.AppendChild($addElement)
        $xd.Save($webconfig) # save changes
	}
}

function UpdateBatchConfig([string]$webroot,[string]$uselazytypeloader,[switch]$addprobingpath)
{
    Write-Log "Updating the Dynamics Batch configuration file."
    $batchconfig=join-path $webroot "bin\batch.exe.config"
    if(!(Test-Path $batchconfig)){
        Write-Log "Batch.exe.config is not found at $webroot\bin"
        return
    } 

    [System.Xml.XmlDocument] $xd=new-object System.Xml.XmlDocument
    $xd.Load($batchconfig)
    $ns=New-Object System.Xml.XmlNamespaceManager($xd.NameTable)
    $ns.AddNamespace("ns",$xd.DocumentElement.NamespaceURI)

    # add UseLazyTypeLoader setting
    $save=$false # flag to indicate if there are changes to save
    $key="UseLazyTypeLoader"
    $value=$uselazytypeloader
    $existingNode = $xd.SelectSingleNode("//ns:add[@key='$key']",$ns)

    if($existingNode -ne $null){
        $existingValue=$existingNode.GetAttribute("value")
        if($existingValue -ne $value){  # update if the existing value -ne new value
            Write-Log "Updating configuration '$key'='$value'"
            $existingNode.SetAttribute("value",$value)
            $save=$true
        }
    }
    else{
        # add UseLazyTypeLoader key
        $addElement=$xd.CreateElement("add")
        $addElement.SetAttribute("key",$key)
        $addElement.SetAttribute("value",$value)
        $appSettings=$xd.SelectSingleNode("//ns:appSettings",$ns)
        Write-Log "Adding configuration '$key'='$value' to Batch.exe.config"
        $appSettings.AppendChild($addElement)
        $save=$true
    }
    
    if($addprobingpath){
        # add appassemblies to the probing path
        $appassemblies="appassemblies"
        $probingelement=$xd.Configuration.RunTime.AssemblyBinding.Probing
        if($probingelement -ne $null){
            [string]$privatepath=$xd.Configuration.RunTime.AssemblyBinding.Probing.privatePath
            [string[]] $probingpaths=$privatepath.Split(";")
            if(-not $probingpaths.Contains($appassemblies)){
                Write-Log "Adding $appassemblies to the private probing path in Batch.exe.config"
                $privatepath += ";$appassemblies"
                $xd.Configuration.RunTime.AssemblyBinding.Probing.privatePath=$privatepath
                $save=$true
            }
        }
        else{
            # add probing element
            $assemblyBinding=$xd.Configuration.RunTime.AssemblyBinding
            $xmlns=$assemblyBinding.xmlns
            $probingElement=$xd.CreateElement("probing")
            $probingElement.SetAttribute("privatePath",$appassemblies)
            $probingElement.SetAttribute("xmlns",$xmlns)
            Write-Log "Adding private probing path to the batch.exe.config"
            $assemblyBinding.AppendChild($probingelement)
            $save=$true
        }
    }

    if($save -eq $true){
        Write-Log "Saving changes to the Dynamics Batch configuration file."
        $xd.Save($batchconfig)
    }
}

$Global:log=$log
InitializeLog
CreateSymbolicLink -webroot:$webroot -packagedir:$packagedir
NgenFiles -webroot:$webroot -updateprobingpath
UpdateWebconfig -webroot:$webroot -uselazytypeloader:"false"
UpdateBatchConfig -webroot:$webroot -uselazytypeloader:"false" -addprobingpath