### Licensed to the Apache Software Foundation (ASF) under one or more
### contributor license agreements.  See the NOTICE file distributed with
### this work for additional information regarding copyright ownership.
### The ASF licenses this file to You under the Apache License, Version 2.0
### (the "License"); you may not use this file except in compliance with
### the License.  You may obtain a copy of the License at
###
###     http://www.apache.org/licenses/LICENSE-2.0
###
### Unless required by applicable law or agreed to in writing, software
### distributed under the License is distributed on an "AS IS" BASIS,
### WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
### See the License for the specific language governing permissions and
### limitations under the License.

<#
.SYNOPSIS
A simple self-contained installation tool for script-based installation on Windows

.EXAMPLE
winpkg helloworld.winpkg.zip install
Runs the install action in the helloworld.winpkg.zip archive.

.EXAMPLE
winpkg helloworld.winpkg.zip help
Lists the available actions in the helloworld.winpkg.zip archive.

.EXAMPLE
winpkg archive.zip utils unzip c:\temp\archive
Unzips the contents of archive.zip into c:\temp\archive
#>

param(
	[parameter( Position=0, Mandatory=$true)]
	[string]$Package, 

	[parameter( Position=1, Mandatory=$true)]
	[string]$Action, 
	[parameter(ValueFromRemainingArguments=$true)] $ArgumentList
	)

###
### COMMON FUNCTIONS -- these are duplicated in Winpkg.Utils.psm1
###

function Write-Log ($message, $level, $pipelineObj )
{
	$ComponentName = "WINPKG"
        $message = SanitizeString $message

	switch($level)
	{
		"Failure" 
		{
			$message = "$ComponentName FAILURE: $message"
			Write-Error $message 
			break;
		}

		"Info"
		{
			$message = "${ComponentName}: $message"
			Write-Host $message
			break;
		}

		default
		{
			$message = "${ComponentName}: $message"
			Write-Host "$message"
		}
	}

	
    Out-File -FilePath $ENV:WINPKG_LOG -InputObject "$message" -Append -Encoding "UTF8"

    if( $pipelineObj -ne $null )
    {
        Out-File -FilePath $ENV:WINPKG_LOG -InputObject $pipelineObj.InvocationInfo.PositionMessage -Append -Encoding "UTF8"
    }
}

function Write-LogRecord( $source, $record )
{
	if( $record -is [Management.Automation.ErrorRecord])
	{
		$message = "$ComponentName-$source FAILURE: " + $record.Exception.Message
                $message = SanitizeString $message

		if( $message.EndsWith( [Environment]::NewLine ))
		{
			Write-Host $message -NoNewline
        	[IO.File]::AppendAllText( "$ENV:WINPKG_LOG", "$message", [Text.Encoding]::UTF8 )
        }
        else
        {
        	Write-Host $message
        	Out-File -FilePath $ENV:WINPKG_LOG -InputObject $message -Append -Encoding "UTF8"
        }
	}
	else
	{
		$message = $record
                $message = SanitizeString $message
		Write-Host $message
		Out-File -FilePath $ENV:WINPKG_LOG -InputObject "$message" -Append -Encoding "UTF8"
	}
}


function SanitizeString($s)
{
    $s -replace "-password([a-z0-9]*) (\S*)", '-password$1 ****'
}

function Invoke-Cmd ($command)
{
	Write-Log $command
	$out = cmd.exe /C "$command" 2>&1
	$out | ForEach-Object { Write-LogRecord "CMD" $_ }
	return $out
}

function Invoke-PsChk ($command)
{
	Write-Log $command
	$out = powershell.exe -InputFormat none -Command "$command" 2>&1
	#$out | ForEach-Object { Write-LogRecord "PS" $_ }
	if ($LastExitCode -ne 0)
	{
		throw $out
	}
	return $out
}

###
### END COMMON FUNCTIONS
###

function Initialize-WinpkgEnv( $logName, $winpkgBinPath )
{
	New-Variable -Name unsetWinpkgLog -Value $false -Option Private

	if( -not (Test-Path ENV:WINPKG_LOG ))
    {
        $ENV:WINPKG_LOG="$logName"
	    $unsetWinpkgLog = $true
	    Write-Log "Logging to new log $ENV:WINPKG_LOG" "Info"
    }
    else
    {
	    Write-Log "Logging to existing log $ENV:WINPKG_LOG" "Info"
    }

    if( -not (Test-Path ENV:WINPKG_BIN))
    {
    	if( $winpkgBinPath -ne $null )
    	{
	    	$ENV:WINPKG_BIN = $winpkgBinPath
	    	New-Variable -Name unsetWinpkgBin -Value $true -Option Private
	    	Write-Log "Setting ENV:WINPKG_BIN to $winpkgBinPath"
	    }
	    else
	    {
	    	throw "Winpkg Bin Path cannot be null"
	    }

    }

    Write-Log "ENV:WINPKG_BIN is $ENV:WINPKG_BIN" "Info"
    return $unsetWinpkgLog
}


function Get-PackageDirectory($package)
{
    $package = Resolve-Path $package
    return Split-Path $package -Parent
}

function Get-PackageName($package)
{
    $package = Resolve-Path $package
    $packageFile = Split-Path $package -Leaf
    return [IO.Path]::GetFileNameWithoutExtension($packageFile)
}

function Get-UnzipRoot ($package, $destination)
{
    $destination = Resolve-Path $destination
    $packageName = Get-PackageName $package
    return Join-Path $destination $packageName;
}

function Cleanup-Package( $unzipRoot )
{
	if( Test-Path $unzipRoot )
    {
        $cmd = "rd /s /q $unzipRoot"
	    Invoke-Cmd $cmd    
    }
}

# Unzips an archive from a source to a destination
#(e.g. Unzip-Archive c:\foo\bar.winpkg c:\packages
function Unzip-Archive ($source, $destination)
{
    if( $source -eq $null )
    {
        throw "Unzip-Archive: source cannot be null"
    }

    if( $destination -eq $null )
    {
        throw "Unzip-Archive: destination cannot be null"
    }

    $source = Resolve-Path $source
    $destination = Resolve-Path $destination
    $unzipRoot = Get-UnzipRoot $source $destination

    Write-Log "UNZIP: source $source"
	Write-Log "UNZIP: destination $destination"
	Write-Log "UNZIP: unzipRoot $unzipRoot"

	Unzip-ToDirectory $source $unzipRoot
	
	Write-Log "Unzip of $source to $destination succeeded"
    Write-Log "UnzipRoot: $unzipRoot"

    return $unzipRoot
}

# Unzips an archive to a specific top-level directory,
# and flattens the top level of archive contents into 
# that directory. This is a destructive operation.

function Unzip-ToDirectory( $source, $unzipRoot )
{
    if( -not (Test-Path $source))
    {
        throw "UNZIP: source $source does not exist"
    }

    try
    {
        $zipFile = New-Object Ionic.Zip.Zipfile $source
        $zipFile.ExtractAll( $unzipRoot, [Ionic.Zip.ExtractExistingFileAction]::OverwriteSilently )
    }
    finally
    {
       if( $zipFile -ne $null )
       {
            $zipFile.Dispose()
       }
    }
}

function Zip-Directory( $source )
{
    if( -not (Test-Path $source -PathType Container))
    {
        throw "$source is does not exist or is not a folder"
    }

    $sourceName = Split-Path $source -Leaf
    $sourceDir = Split-Path $source -Parent
    $destination = Join-Path $sourceDir "$sourceName.zip"

    $recurse = $true


    try
    {
        $zipFile = New-Object Ionic.Zip.ZipFile

        foreach( $item in (Get-ChildItem $source))
        {
           Write-Log "Adding item $item"
           [Void] $zipFile.AddItem( $item.FullName, $item.Name )
        }
    
        [Void] $zipFile.Save( $destination )
    }
    finally
    {
        if( $zipFile -ne $null )
        {
            $zipFile.Dispose()
        }
    }
}

###
### ACTION PROCESSING
###

function Test-BuiltInAction( $action )
{
	$builtins = ("help", "utils")
    return $builtins -contains $action
}

function Run-BuiltInAction($package, $destination, $action, $arguments)
{
	switch( $action )
	{
		"help" {
			try
			{
				$unzipRoot = Unzip-Archive $package $destination
				Show-Help $unzipRoot $arguments
			}
			catch [Exception] 
			{
				Write-Log $_.Exception.Message "Failure" $_
			}
			finally
			{
				Cleanup-Package $unzipRoot
			}
			break;
		}
		"utils" {
			$action, $arguments = $arguments
			Run-UtilsAction $package $destination $action $arguments
		}
	}
}

function Show-Help( $unzipRoot, $arguments )
{
	if( $arguments -eq $null )
	{
		$scripts = Join-Path $unzipRoot "scripts"
		foreach( $script in (Get-ChildItem $scripts))
		{
			$action = [IO.Path]::GetFileNameWithoutExtension( $script )
			Write-Host $action
		}
	}
	else
	{
		$script, $restOfArgs = $arguments

		$scriptPath = Join-Path $unzipRoot ${script}.ps1

		if( Test-Path $scriptPath )
		{
			Get-Help $scriptPath
		}
		else
		{
			Write-Host "No additional help for action $script available"
		}
	}
}


function Run-UtilsAction($package, $destination, $action, $arguments)
{
	switch($action)
	{
		"unzip" 
		{
			$destination, $restOfArgs = $arguments

			if( $destination -eq $null )
			{
				throw "unzip: destination required"
			}

            $package = [IO.Path]::GetFullPath( $package );
			$destination = [IO.Path]::GetFullPath( $destination )

			Write-Log "Preparing to unzip $package to $destination"

			if( -not (Test-Path $destination))
			{
				New-Item $destination -type Directory > $null
			}

			Unzip-ToDirectory $package $destination
			break;
		}
        zip
        {
            $package = [IO.Path]::GetFullPath( $package );

			Zip-Directory $package
			break;
        }

		default 
		{
			throw "Unknown utility action $action"
		}
	}
}

function Run-WinpkgAction ($package, $destination, $action, $arguments)
{
        $cmd = ''
        $actionExitCode = 1
	try
	{
		if( Test-Path $package -PathType Container )
		{
            New-Variable -Name skipCleanup -Value $true -Option Private
			$unzipRoot = $package
		}
		else
		{
			$unzipRoot = Unzip-Archive $package $destination
		}

		if( -not (Test-Path "$unzipRoot\scripts"))
	    {
		    Write-Log "Malformed winpkg: Failed to find scripts directory" "Failure"
		    exit 1
	    }

	    Push-Location "$unzipRoot\scripts"
	    New-Variable -Name locationPushed -Value $true -Option Private

		if( ($action -eq "help") -or ($action -eq "/?") -or ($action -eq "-?"))
		{
			Show-Help $unzipRoot
		}
		elseif( Test-Path "${action}.ps1" )
		{
			$path = Resolve-Path "${action}.ps1"
			$cmd = "$path $arguments"
			Invoke-PsChk $cmd
		}
		elseif( Test-Path "${action}.cmd" )
		{
			$path = Resolve-Path "${action}.cmd"
			$cmd = "$path $arguments"
			Invoke-Cmd $cmd
		}
		elseif( Test-Path "${action}.bat" )
		{
			$path = Resolve-Path "${action}.bat"
			$cmd = "$path $arguments"
			Invoke-Cmd $cmd
		}
		else
		{
			Write-Log "Unknown action $action" "Failure"
			Write-Log "Current directory is $pwd"
			Show-Help $pwd
			exit 1
		}
	}
	catch [Exception]
	{
		Write-Log $_.Exception.Message "Failure" $_
		exit 1
	}
	finally
	{
        $actionExitCode = $lastExitCode
		if( $locationPushed )
		{
			Pop-Location
		}

        if( -not $skipCleanup )
        {
		    Cleanup-Package $unzipRoot
        }

        if ($actionExitCode)
        {
            Write-Log "$cmd failed." "Failure"
            exit $actionExitCode
        }
	}
}

###
### SCRIPT MAIN
###

function Main( $winpkgBinPath, $Package, $Action, $ArgumentList)
{
    try
    {
    	$packagename = Get-PackageName $Package
    	$unsetWinpkgLog = Initialize-WinpkgEnv "$pwd\$packagename.$Action.log" $winpkgBinPath 
	
	    Write-Log "Setting Environment::CurrentDirectory to $pwd"
	    [Environment]::CurrentDirectory = $pwd

	    Write-Log "Current Directory: $pwd"
	    Write-Log "Package: $Package"
	    Write-Log "Action: $Action"
	    Write-Log "Action arguments: $ArgumentList"

	    if( -not (Test-Path $Package))
	    {
		    Write-Log "package $Package does not exist" "Failure"
		    exit 1
	    }
		if ($Action -ieq "unzip")
		{
			Unzip-Archive $package $ArgumentList
		}
		else 
		{
		    $packageDirectory = Split-Path (Resolve-Path $Package) -Parent

		    if( Test-BuiltInAction $Action )
		    {
			    Write-Log "Run-BuiltInAction $Package $ArgumentList $Action"
			    Run-BuiltInAction $Package $ArgumentList $Action  
		    }
		    else
		    {
			    Write-Log "Run-WinpkgAction $Package $ArgumentList $Action"
	            Run-WinpkgAction $Package $ArgumentList $Action
		    }
		}
		    $exitCode = $lastExitCode
		    Write-Log "Finished processing $Package"
		    exit $exitCode
    }
    catch [Exception]
    {
	    Write-Log $_.Exception.Message "Failure" $_
	    exit 1
    }
    finally
    {
	    if( $unsetWinpkgBin )
	    {
		    Write-Log "Removing ENV:WINPKG_BIN"
		    Remove-Item ENV:WINPKG_BIN
	    }

	    if( $unsetWinpkgLog )
	    {
		    Write-Log "Removing ENV:WINPKG_LOG"
		    Remove-Item ENV:WINPKG_LOG
	    }
    }
}

try
{
	#Ionic.Zip.dll v1.9.1.8 from http://dotnetzip.codeplex.com/
	#Licensed under Microsoft Public License (MS-PL): http://dotnetzip.codeplex.com/license
	#Included here as a base64-encoded string
    $ionicZipDll = "[encoded data, 616448 characters]"

    $winpkgBinPath = Split-Path $MyInvocation.MyCommand.Path

    if( -not (Test-Path Variable:IonicZipAssembly ))
    {
        $bytes = [Convert]::FromBase64String( $ionicZipDll )
        $ionicZipAssembly = [Reflection.Assembly]::Load( $bytes )
    }

    Main $winpkgBinPath $Package $Action $ArgumentList
}
catch
{
	Write-Log $_.Exception.Message "Failure" $_
	exit 1
}
finally
{
    $winpkgBinPath = $null

    if( $unsetWinpkgLog )
    {
    	Remove-Item ENV:WINPKG_LOG
    }
}
