/*
 * Copyright (C) 2012 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

/**
 * @file
 *
 * Main entry point
 *
 */

#include <stdint.h>
#include <stddef.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include "wimboot.h"
#include "peloader.h"
#include "int13.h"
#include "vdisk.h"
#include "cpio.h"
#include "lznt1.h"
#include "xca.h"
#include "cmdline.h"
#include "patch.h"
#include "wimfile.h"
#include "pause.h"
#include "paging.h"
#include "memmap.h"

/** Start of our image (defined by linker) */
extern char _start[];

/** End of our image (defined by linker) */
extern char _end[];

/** Command line */
char *cmdline;

/** initrd */
void *initrd;

/** Length of initrd */
size_t initrd_len;

/** bootmgr.exe path within WIM */
static const wchar_t bootmgr_path[] = L"\\Windows\\Boot\\PXE\\bootmgr.exe";

/** Other paths within WIM */
static const wchar_t *wim_paths[] = {
	L"\\Windows\\Boot\\DVD\\PCAT\\boot.sdi",
	L"\\Windows\\Boot\\DVD\\PCAT\\BCD",
	L"\\Windows\\Boot\\Fonts\\segmono_boot.ttf",
	L"\\Windows\\Boot\\Fonts\\segoen_slboot.ttf",
	L"\\Windows\\Boot\\Fonts\\segoe_slboot.ttf",
	L"\\Windows\\Boot\\Fonts\\wgl4_boot.ttf",
	L"\\sms\\boot\\boot.sdi",
	NULL
};

/** bootmgr.exe file */
static struct vdisk_file *bootmgr;

/** WIM image file */
static struct vdisk_file *bootwim;

/** Minimal length of embedded bootmgr.exe */
#define BOOTMGR_MIN_LEN 16384

/** 1MB memory threshold */
#define ADDR_1MB 0x00100000

/** 2GB memory threshold */
#define ADDR_2GB 0x80000000

/** Memory regions */
enum {
	WIMBOOT_REGION = 0,
	PE_REGION,
	LDR_REGION,
	INITRD_REGION,
	NUM_REGIONS
};

/**
 * Wrap interrupt callback
 *
 * @v params		Parameters
 */
static void call_interrupt_wrapper ( struct bootapp_callback_params *params ) {
	struct paging_state state;
	uint16_t *attributes;

	/* Handle/modify/pass-through interrupt as required */
	if ( params->vector.interrupt == 0x13 ) {

		/* Enable paging */
		enable_paging ( &state );

		/* Intercept INT 13 calls for the emulated drive */
		emulate_int13 ( params );

		/* Disable paging */
		disable_paging ( &state );

	} else if ( ( params->vector.interrupt == 0x10 ) &&
		    ( params->ax == 0x4f01 ) &&
		    ( ! nt_cmdline->gui ) ) {

		/* Mark all VESA video modes as unsupported */
		attributes = REAL_PTR ( params->es, params->di );
		call_interrupt ( params );
		*attributes &= ~0x0001;

	} else {

		/* Pass through interrupt */
		call_interrupt ( params );
	}
}

/** Real-mode callback functions */
static struct bootapp_callback_functions callback_fns = {
	.call_interrupt = call_interrupt_wrapper,
	.call_real = call_real,
};

/** Real-mode callbacks */
static struct bootapp_callback callback = {
	.fns = &callback_fns,
};

/** Boot application descriptor set */
static struct {
	/** Boot application descriptor */
	struct bootapp_descriptor bootapp;
	/** Boot application memory descriptor */
	struct bootapp_memory_descriptor memory;
	/** Boot application memory descriptor regions */
	struct bootapp_memory_region regions[NUM_REGIONS];
	/** Boot application entry descriptor */
	struct bootapp_entry_descriptor entry;
	struct bootapp_entry_wtf1_descriptor wtf1;
	struct bootapp_entry_wtf2_descriptor wtf2;
	struct bootapp_entry_wtf3_descriptor wtf3;
	struct bootapp_entry_wtf3_descriptor wtf3_copy;
	/** Boot application callback descriptor */
	struct bootapp_callback_descriptor callback;
	/** Boot application pointless descriptor */
	struct bootapp_pointless_descriptor pointless;
} __attribute__ (( packed )) bootapps = {
	.bootapp = {
		.signature = BOOTAPP_SIGNATURE,
		.version = BOOTAPP_VERSION,
		.len = sizeof ( bootapps ),
		.arch = BOOTAPP_ARCH_I386,
		.memory = offsetof ( typeof ( bootapps ), memory ),
		.entry = offsetof ( typeof ( bootapps ), entry ),
		.xxx = offsetof ( typeof ( bootapps ), wtf3_copy ),
		.callback = offsetof ( typeof ( bootapps ), callback ),
		.pointless = offsetof ( typeof ( bootapps ), pointless ),
	},
	.memory = {
		.version = BOOTAPP_MEMORY_VERSION,
		.len = sizeof ( bootapps.memory ),
		.num_regions = NUM_REGIONS,
		.region_len = sizeof ( bootapps.regions[0] ),
		.reserved_len = sizeof ( bootapps.regions[0].reserved ),
	},
	.entry = {
		.signature = BOOTAPP_ENTRY_SIGNATURE,
		.flags = BOOTAPP_ENTRY_FLAGS,
	},
	.wtf1 = {
		.flags = 0x11000001,
		.len = sizeof ( bootapps.wtf1 ),
		.extra_len = ( sizeof ( bootapps.wtf2 ) +
			       sizeof ( bootapps.wtf3 ) ),
	},
	.wtf3 = {
		.flags = 0x00000006,
		.len = sizeof ( bootapps.wtf3 ),
		.boot_partition_offset = ( VDISK_VBR_LBA * VDISK_SECTOR_SIZE ),
		.xxx = 0x01,
		.mbr_signature = VDISK_MBR_SIGNATURE,
	},
	.wtf3_copy = {
		.flags = 0x00000006,
		.len = sizeof ( bootapps.wtf3 ),
		.boot_partition_offset = ( VDISK_VBR_LBA * VDISK_SECTOR_SIZE ),
		.xxx = 0x01,
		.mbr_signature = VDISK_MBR_SIGNATURE,
	},
	.callback = {
		.callback = &callback,
	},
	.pointless = {
		.version = BOOTAPP_POINTLESS_VERSION,
	},
};

/**
 * Test if a paragraph is empty
 *
 * @v pgh		Paragraph
 * @ret is_empty	Paragraph is empty (all zeroes)
 */
static int is_empty_pgh ( const void *pgh ) {
	const uint32_t *dwords = pgh;

	return ( ( dwords[0] | dwords[1] | dwords[2] | dwords[3] ) == 0 );
}

/**
 * Read from file
 *
 * @v file		Virtual file
 * @v data		Data buffer
 * @v offset		Offset
 * @v len		Length
 */
static void read_file ( struct vdisk_file *file, void *data, size_t offset,
			size_t len ) {

	memcpy ( data, ( file->opaque + offset ), len );
}

/**
 * Add embedded bootmgr.exe extracted from bootmgr
 *
 * @v data		File data
 * @v len		Length
 * @ret file		Virtual file, or NULL
 *
 * bootmgr.exe is awkward to obtain, since it is not available as a
 * standalone file on the installation media, or on an installed
 * system, or in a Windows PE image as created by WAIK or WADK.  It
 * can be extracted from a typical boot.wim image using ImageX, but
 * this requires installation of the WAIK/WADK/wimlib.
 *
 * A compressed version of bootmgr.exe is contained within bootmgr,
 * which is trivial to obtain.
 */
static struct vdisk_file * add_bootmgr ( const void *data, size_t len ) {
	const uint8_t *compressed;
	size_t offset;
	size_t compressed_len;
	ssize_t ( * decompress ) ( const void *data, size_t len, void *buf );
	ssize_t decompressed_len;
	size_t padded_len;

	/* Look for an embedded compressed bootmgr.exe on an
	 * eight-byte boundary.
	 */
	for ( offset = BOOTMGR_MIN_LEN ; offset < ( len - BOOTMGR_MIN_LEN ) ;
	      offset += 0x08 ) {

		/* Initialise checks */
		decompress = NULL;
		compressed = ( data + offset );
		compressed_len = ( len - offset );

		/* Check for an embedded LZNT1-compressed bootmgr.exe.
		 * Since there is no way for LZNT1 to compress the
		 * initial "MZ" bytes of bootmgr.exe, we look for this
		 * signature starting three bytes after a paragraph
		 * boundary, with a preceding tag byte indicating that
		 * these two bytes would indeed be uncompressed.
		 */
		if ( ( ( offset & 0x0f ) == 0x00 ) &&
		     ( ( compressed[0x02] & 0x03 ) == 0x00 ) &&
		     ( compressed[0x03] == 'M' ) &&
		     ( compressed[0x04] == 'Z' ) ) {
			DBG ( "...checking for LZNT1-compressed bootmgr.exe at "
			      "+%#zx\n", offset );
			decompress = lznt1_decompress;
		}

		/* Check for an embedded XCA-compressed bootmgr.exe.
		 * The bytes 0x00, 'M', and 'Z' will always be
		 * present, and so the corresponding symbols must have
		 * a non-zero Huffman length.  The embedded image
		 * tends to have a large block of zeroes immediately
		 * beforehand, which we check for.  It's implausible
		 * that the compressed data could contain substantial
		 * runs of zeroes, so we check for that too, in order
		 * to eliminate some common false positive matches.
		 */
		if ( ( ( compressed[0x00] & 0x0f ) != 0x00 ) &&
		     ( ( compressed[0x26] & 0xf0 ) != 0x00 ) &&
		     ( ( compressed[0x2d] & 0x0f ) != 0x00 ) &&
		     ( is_empty_pgh ( compressed - 0x10 ) ) &&
		     ( ! is_empty_pgh ( ( compressed + 0x400 ) ) ) &&
		     ( ! is_empty_pgh ( ( compressed + 0x800 ) ) ) &&
		     ( ! is_empty_pgh ( ( compressed + 0xc00 ) ) ) ) {
			DBG ( "...checking for XCA-compressed bootmgr.exe at "
			      "+%#zx\n", offset );
			decompress = xca_decompress;
		}

		/* If we have not found a possible bootmgr.exe, skip
		 * to the next offset.
		 */
		if ( ! decompress )
			continue;

		/* Find length of decompressed image */
		decompressed_len = decompress ( compressed, compressed_len,
						NULL );
		if ( decompressed_len < 0 ) {
			/* May be a false positive signature match */
			continue;
		}

		/* Prepend decompressed image to initrd */
		DBG ( "...extracting embedded bootmgr.exe\n" );
		padded_len = ( ( decompressed_len + PAGE_SIZE - 1 ) &
			       ~( PAGE_SIZE - 1 ) );
		initrd -= padded_len;
		initrd_len += padded_len;
		decompress ( compressed, compressed_len, initrd );

		/* Add decompressed image */
		return vdisk_add_file ( "bootmgr.exe", initrd,
					decompressed_len, read_file );
	}

	DBG ( "...no embedded bootmgr.exe found\n" );
	return NULL;
}

/**
 * File handler
 *
 * @v name		File name
 * @v data		File data
 * @v len		Length
 * @ret rc		Return status code
 */
static int add_file ( const char *name, void *data, size_t len ) {
	struct vdisk_file *file;

	/* Store file */
	file = vdisk_add_file ( name, data, len, read_file );

	/* Check for special-case files */
	if ( strcasecmp ( name, "bootmgr.exe" ) == 0 ) {
		DBG ( "...found bootmgr.exe\n" );
		bootmgr = file;
	} else if ( strcasecmp ( name, "bootmgr" ) == 0 ) {
		DBG ( "...found bootmgr\n" );
		if ( ( ! bootmgr ) &&
		     ( bootmgr = add_bootmgr ( data, len ) ) ) {
			DBG ( "...extracted bootmgr.exe\n" );
		}
	} else if ( strcasecmp ( name, "BCD" ) == 0 ) {
		DBG ( "...found BCD\n" );
		vdisk_patch_file ( file, patch_bcd );
	}

	return 0;
}

/**
 * Relocate data between 1MB and 2GB if possible
 *
 * @v data		Start of data
 * @v len		Length of data
 * @ret start		Start address
 */
static void * relocate_memory_low ( void *data, size_t len ) {
	struct e820_entry *e820 = NULL;
	uint64_t end;
	intptr_t start;

	/* Read system memory map */
	while ( ( e820 = memmap_next ( e820 ) ) != NULL ) {

		/* Find highest compatible placement within this region */
		end = ( e820->start + e820->len );
		start = ( ( end > ADDR_2GB ) ? ADDR_2GB : end );
		if ( start < len )
			continue;
		start -= len;
		start &= ~( PAGE_SIZE - 1 );
		if ( start < e820->start )
			continue;
		if ( start < ADDR_1MB )
			continue;

		/* Relocate to this region */
		memmove ( ( void * ) start, data, len );
		return ( ( void * ) start );
	}

	/* Leave at original location */
	return data;
}

/**
 * Main entry point
 *
 */
int main ( void ) {
	size_t padded_len;
	void *raw_pe;
	struct loaded_pe pe;
	struct paging_state state;
	uint64_t initrd_phys;

	/* Initialise stack cookie */
	init_cookie();

	/* Print welcome banner */
	printf ( "\n\nwimldr\n\n" );

	/* Process command line */
	process_cmdline ( cmdline );

	/* Initialise paging */
	init_paging();

	/* Enable paging */
	enable_paging ( &state );

	/* Relocate initrd below 2GB if possible, to avoid collisions */
	DBG ( "Found initrd at [%p,%p)\n", initrd, ( initrd + initrd_len ) );
	initrd = relocate_memory_low ( initrd, initrd_len );
	DBG ( "Placing initrd at [%p,%p)\n", initrd, ( initrd + initrd_len ) );

	/* Extract files from initrd */
	if ( cpio_extract ( nt_cmdline->ldr_data, nt_cmdline->ldr_len, add_file ) != 0 )
		die ( "FATAL: could not extract initrd files\n" );

	/* Extract boot.wim from initrd */
	bootwim = vdisk_add_file ( "boot.wim", initrd, initrd_len,
							   read_file );

	/* Process WIM image */
	if ( bootwim ) {
		vdisk_patch_file ( bootwim, patch_wim );
		if ( ( ! bootmgr ) &&
		     ( bootmgr = wim_add_file ( bootwim, nt_cmdline->index,
						bootmgr_path,
						L"bootmgr.exe" ) ) ) {
			DBG ( "...extracted bootmgr.exe\n" );
		}
		wim_add_files ( bootwim, nt_cmdline->index, wim_paths );
	}

	/* Add INT 13 drive */
	callback.drive = initialise_int13();

	/* Read bootmgr.exe into memory */
	if ( ! bootmgr )
		die ( "FATAL: no bootmgr.exe\n" );
	if ( bootmgr->read == read_file ) {
		raw_pe = bootmgr->opaque;
	} else {
		padded_len = ( ( bootmgr->len + PAGE_SIZE - 1 ) &
			       ~( PAGE_SIZE -1 ) );
		raw_pe = ( initrd - padded_len );
		bootmgr->read ( bootmgr, raw_pe, 0, bootmgr->len );
	}

	/* Load bootmgr.exe into memory */
	if ( load_pe ( raw_pe, bootmgr->len, &pe ) != 0 )
		die ( "FATAL: Could not load bootmgr.exe\n" );

	/* Relocate initrd above 4GB if possible, to free up 32-bit memory */
	initrd_phys = relocate_memory_high ( initrd, initrd_len );
	DBG ( "Placing initrd at physical [%#llx,%#llx)\n",
	      initrd_phys, ( initrd_phys + initrd_len ) );

	/* Complete boot application descriptor set */
	bootapps.bootapp.pe_base = pe.base;
	bootapps.bootapp.pe_len = pe.len;
	bootapps.regions[WIMBOOT_REGION].start_page = page_start ( _start );
	bootapps.regions[WIMBOOT_REGION].num_pages = page_len ( _start, _end );
	bootapps.regions[PE_REGION].start_page = page_start ( pe.base );
	bootapps.regions[PE_REGION].num_pages =
		page_len ( pe.base, ( pe.base + pe.len ) );
	bootapps.regions[LDR_REGION].start_page =
		page_start ( nt_cmdline->ldr_data );
	bootapps.regions[LDR_REGION].num_pages =
		page_len ( nt_cmdline->ldr_data,
				   nt_cmdline->ldr_data + nt_cmdline->ldr_len );
	bootapps.regions[INITRD_REGION].start_page =
		( initrd_phys / PAGE_SIZE );
	bootapps.regions[INITRD_REGION].num_pages =
		page_len ( initrd, initrd + initrd_len );

	/* Omit initrd region descriptor if located above 4GB */
	if ( initrd_phys >= ADDR_4GB )
		bootapps.memory.num_regions--;

	/* Disable paging */
	disable_paging ( &state );

	/* Jump to PE image */
	DBG ( "Entering bootmgr.exe with parameters at %p\n", &bootapps );
	if ( nt_cmdline->pause )
		pause();
	pe.entry ( &bootapps.bootapp );
	die ( "FATAL: bootmgr.exe returned\n" );
}
