<?php

use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;

use App\Models\{Address,Domain,Setup};

/**
 * Calculate CCITT-CRC16 checksum
 */
if (! function_exists('crc16')) {
	function crc16($data): int
	{
		$crc = 0x0000;

		for ($i = 0; $i < strlen($data); $i++) {
			$x = (($crc >> 8) ^ ord($data[$i])) & 0xFF;
			$x ^= $x >> 4;
			$crc = (($crc << 8) ^ ($x << 12) ^ ($x << 5) ^ $x) & 0xFFFF;
		}

		return $crc;
	}
}

/**
 * Dump out data into a hex dump
 */
if (! function_exists('hex_dump')) {
	function hex_dump($data,$newline="\n",$width=16): string
	{
		$result = '';

		$pad = '.'; # padding for non-visible characters
		$to = $from = '';

		for ($i=0; $i<=0xFF; $i++) {
			$from .= chr($i);
			$to .= ($i >= 0x20 && $i <= 0x7E) ? chr($i) : $pad;
		}

		$hex = str_split(bin2hex($data),$width*2);
		$chars = str_split(strtr($data,$from,$to),$width);

		$offset = 0;
		foreach ($hex as $i => $line) {
			$result .= sprintf('%08X: %-48s  [%s]%s',
				$offset,
				substr_replace(implode(' ',str_split($line,2)),' ',8*3,0),
				$chars[$i],
				$newline);

			$offset += $width;
		}

		return $result;
	}
}

/**
 * Send a value has hex chars
 */
if (! function_exists('hexstr')) {
	function hexstr(int $int): string
	{
		if ($int > 0xffff)
			throw new Exception('Int too large for hexstr');

		$hexdigitslower = '0123456789abcdef';
		$x = '';

		if ($int > 0xff) {
			$x .= substr($hexdigitslower,($int&0xf000)>>12,1);
			$x .= substr($hexdigitslower,($int&0x0f00)>>8,1);
		}

		$x .= substr($hexdigitslower,($int&0x00f0)>>4,1);
		$x .= substr($hexdigitslower,($int&0x000f),1);

		return $x;
	}
}

/**
 * Return our addresses.
 * If domain provided, limit the list to those within the domain - returning a Collection::class
 * If address provided, return our address that would be used for the provided address - return Address::class
 *
 * @param Domain|Address|null $o - Domain or Address
 * @return Collection|Address|NULL
 * @throws Exception
 */
function our_address(Domain|Address $o=NULL): Collection|Address|NULL
{
	if (! Config::has('setup'))
		Config::set('setup',Setup::findOrFail(config('app.id')));

	$so = Config::get('setup');
	$so->loadMissing([
		'system:id,name,sysop,location',
		'system.akas:addresses.id,addresses.zone_id,region_id,host_id,node_id,point_id,addresses.system_id,addresses.active,role',
		'system.akas.zone:id,domain_id,zone_id',
		'system.akas.zone.domain:id,name',
	]);

	// If we dont have any addresses
	if ($so->system->akas->count() === 0)
		return NULL;

	// We havent asked for an address/domain, so we'll return them all.
	if (is_null($o))
		return $so->system->akas;

	// We are requesting a list of addresses for a Domain, or a specific Address, and we have more than 1
	switch (get_class($o)) {
		// Looking for addresses in the same domain, and if fido.strict, addresses that have a higher role (ie: uplink)
		case Address::class:
			$filter = $so->system->akas
				->filter(fn($item)=>$item->zone->domain_id === $o->zone->domain_id)
				->sortBy('role_id');

			// If we are looking for a specific address, and there is only 1 result, return it, otherwise return what we have
			if (config('fido.strict') && ($x=$filter->filter(fn($item)=>$item->role_id <= $o->role_id)->sortBy('role_id'))->count())
				$filter = $x;

			return $filter->count() ? $filter->last()->unsetRelation('nodes_hub') : NULL;

		// Addresses in this domain
		case Domain::class:
			return $so->system->akas
				->filter(fn($item)=>$item->zone->domain_id === $o->id)
				->sortBy('role_id');

		// We shouldnt get here
		default:
			throw new Exception('Unhandled class: '.get_class($o));
	}
}

function our_hostname(Address $o): string
{
	$our = our_address($o->domain)->first();

	switch ($our->role_id) {
		case Address::NODE_ZC:
			$domain = collect(explode('.',gethostname()))->forget(0)
				->prepend(sprintf('z%d',$our->zone->zone_id));
			break;

		case Address::NODE_RC:
		case Address::NODE_NC:
			$domain = collect(explode('.',gethostname()))->forget(0)
				->prepend(sprintf('z%d',$our->zone->zone_id))
				->prepend(sprintf('n%d',$our->host_id));
			break;

		case Address::NODE_HC:
			$domain = collect(explode('.',gethostname()))->forget(0)
				->prepend(sprintf('z%d',$our->zone->zone_id))
				->prepend(sprintf('n%d',$our->host_id))
				->prepend(sprintf('f%d',$our->node_id));
			break;

		default:
			$domain = collect(explode('.',gethostname()));
	}

	return $domain->join('.');
}

/**
 * Return a list of nodes that collect mail directly from us
 *
 * @param Domain|NULL $do
 * @return Collection
 */
function our_nodes(Domain $do=NULL): Collection
{
	return Address::select(['addresses.id','addresses.zone_id','region_id','host_id','node_id','point_id','addresses.system_id','role'])
		->join('system_zone',['system_zone.system_id'=>'addresses.system_id','system_zone.zone_id'=>'addresses.zone_id'])
		->when(! is_null($do),
			fn($query)=>$query
				->join('zones',['zones.id'=>'addresses.zone_id'])
				->where('domain_id',$do->id))
		->active()
		->FTNorder()
		->get();
}

if (! function_exists('timew')) {
	/**
	 * Convert a time into an 32 bit value. This is primarily used to create 8 character hex filenames that
	 * are unique using 1/10th second precision.
	 *
	 * Time is:
	 * + 02 bits least significant bits of year - giving us a 4 year timestamp
	 * + 04 bits Month
	 * + 05 bits Day
	 * + 05 bits Hour
	 * + 06 bits Min
	 * + 06 bits Sec
	 * + 04 bits 10th Sec
	 * = 32 bits
	 *
	 * @param Carbon|null $time
	 * @return int
	 * @todo Since this is used as part of our msgid, we need to use the first 2 bits to get our 3 year unique msgid, ie: year&0x03
	 */
	function timew(Carbon $time=NULL): int
	{
		static $delay = 0;

		// If we are not passed a time, we'll use the time now
		if (! $time) {
			// In case we are called twice, we'll delay 1/10th second so we have a unique result.
			if (Carbon::now()->getPreciseTimestamp(1) === $delay)
				usleep(100000);

			$time = Carbon::now();
		}

		$delay = $time->getPreciseTimestamp(1);

		$t = 0;
		$t = ($t | $time->year & 0x3) << 4;
		$t = ($t | ($time->month & 0xf)) << 5;
		$t = ($t | ($time->day & 0x1f)) << 5;
		$t = ($t | ($time->hour & 0x1f)) << 6;
		$t = ($t | ($time->minute & 0x3f)) << 6;
		$t = ($t | ($time->second & 0x3f)) << 4;
		$t = ($t | ((int)($time->milli/100)) & 0xf);

		return $t;
	}
}

if (! function_exists('wtime')) {
	/**
	 * Convert a 40 bit integer into a time.
	 * We need to filter out loose bits, ie:
	 * + year all bits valid
	 * + month 11xx and 0000 are invalid
	 * + day all bits valid except 0000
	 * + hour 11xxx are invalid
	 * + min 1111xx are invalid
	 * + sec 1111xx are invalid
	 * + 1/2 11xx, 101x are invalid
	 * @see timew()
	 *
	 * @param int $time
	 * @param int|null $year
	 * @return Carbon
	 */
	function wtime(int $time,int $year=NULL): Carbon
	{
		if (! $year)
			$year = Carbon::now()->year;

		// Does the time have milli seconds?
		if ($time > pow(2,26)-1) {
			$milli = ($time & 0xf);
			$time = $time >> 4;
			if ($milli > 9)
				$milli = 9;

		} else {
			$milli = 0;
		}

		$sec = ($time & 0x3f);
		if ($sec > 59)
			$sec = 59;
		$time = $time >> 6;

		$min = ($time & 0x3f);
		if ($min > 59)
			$min = 59;
		$time = $time >> 6;

		$hr = ($time & 0x1f);
		if ($hr > 23)
			$hr = 23;
		$time = $time >> 5;

		$day = ($time & 0x1f);
		$time = $time >> 5;

		$month = ($time & 0xf);
		if ($month > 12)
			$month = 12;
		$time = $time >> 4;

		return Carbon::create(($year & 0xffc)+$time,$month,$day,$hr,$min,$sec+$milli/10);
	}
}