<?php

// Code-hosting website
// ````````````````````

/**
 * Create a thumbnail of an image. It overscales, centers, and crops to fit the
 *   target dimensions.
 * 
 * @param string $src_file
 * @param string $dest_file   Null to return an image handle
 * @param int $width
 * @param int $height
 * @return boolean
 */
function mkthumbnail($src_file, $dest_file, $width, $height) {
	list($src_width, $src_height) = getimagesize($src_file);
	
	$im = imagecreatefromstring(file_get_contents($src_file));
	
	$dest = imagecreatetruecolor($width, $height);
	imagefilledrectangle($dest, 0, 0, $width, $height, imagecolorallocate($dest, 0xFF, 0xFF, 0xFF));
	
	$scale = max( $width/$src_width, $height/$src_height ); // overscale + crop
	
	$box_w = $width/$scale;
	$box_h = $height/$scale; 
	
	$box_xoff = floor(($src_width - $box_w)/2);
	$box_yoff = floor(($src_height - $box_h)/2);
	
	imagecopyresampled(
		$dest, $im,
		0, 0,
		$box_xoff, $box_yoff,
		$width, $height, $box_w, $box_h		
	);
	
	imagedestroy($im);
	
	if (is_null($dest_file)) {
		return $dest;
	} else {
		return imagejpeg($dest, $dest_file);
	}
}

function mkspritesheet(array $handles, $dest_file, $width, $height) {
	$im = imagecreatetruecolor($width, $height * count($handles));
	
	for($i = 0, $e = count($handles); $i != $e; ++$i) {
		imagecopy($im, $handles[$i], 0, $i * $height, 0, 0, $width, $height); 
	}
	
	if (is_null($dest_file)) {
		return $dest_file;
	} else {
		return imagejpeg($im, $dest_file);
	}
}

function fbytes($size, $suffixes='B|KiB|MiB|GiB|TiB') {
	$sxlist = explode('|', $suffixes);
	if ($size < 1024) {
		return $size.$sxlist[0];
	}
	
	while ($size > 1024 && count($sxlist) >= 2) {
		array_shift($sxlist);
		$size /= 1024;
	}
	return number_format($size, 2).array_shift($sxlist);
}

function str_ext($sz) {
	$dpos = strrpos($sz, '.');
	return substr($sz, $dpos+1);
}

function is_image($sz) {
	return in_array(strtolower(str_ext($sz)), ['jpg', 'png', 'jpeg']);
}

function hesc($sz) {
	return @htmlentities($sz, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function text2html($sz) {

	$identity = function($sz) {
		return $sz;
	};
	
	$splitInside = function($begin, $end, $sz) {
		$parts = explode($begin, $sz);
		if (count($parts) == 1) return [$sz];
		
		$ret = [$parts[0]];
		for($i = 1, $e = count($parts); $i !== $e; ++$i) {
			$inner = explode($end, $parts[$i], 2);
			$ret = array_merge($ret, $inner);
		}
		return $ret;
	};
	
	$oddEven = function(array $parts, $odd, $even, $join='') {
		$ret = [];
		for($i = 0, $e = count($parts); $i != $e; ++$i) {
			$ret[] = ($i % 2) ? $odd($parts[$i]) : $even($parts[$i]);
		}
		return implode($join, $ret);
	};
	
	$sectionFmt = function($sz) use($oddEven, $identity) {
		$base = hesc($sz);	
		
		$base = preg_replace('~^=+(.+)=+~m', '<strong>\\1</strong>', $base);
		$base = preg_replace('~(https?://[^ \\r\\n\\t]+)~i', '<a href="\\1">\\1</a>', $base);
		
		$btparts = explode('`', $base);
		if (count($btparts) > 1 && (count($btparts) % 2)) {
			for ($i = 1, $e = count($btparts); $i < $e; $i += 2) {
				$btparts[$i] = '<span class="code">'.$btparts[$i].'</span>';
			}
		}
		
		return $oddEven($btparts, $identity, 'nl2br');
	};
	
	$htmlSections = $splitInside('<html>', '</html>', $sz);
	return $oddEven($htmlSections, $identity, $sectionFmt);
}

function array_decimate($array, $total, $partno) {
	$ct = 0;
	$ret = [];
	foreach($array as $k => $v) {
		if (++$ct % $total == ($partno - 1)) {
			$ret[$k] = $v;
		}
	}
	return $ret;
}

/**
 * 
 */
class CProject {
	
	private $dir;
	public $projname;
	public $shortdesc = '(no description)';
	public $subtag = '';
	public $lastupdate = 0;
	private $longdesc = '';
	private $images = array();
	private $downloads = array();
	private $downloads_hashes = array();
	public $tags = array();
	
	public $homeimage = null;
	
	public function __construct($dirname, $projname) {
		$this->dir = BASEDIR.'data/'.$dirname.'/';
		$this->projname = $projname;
		
		// Identify resources in folder
		
		$ls = scandir($this->dir);
		foreach($ls as $file) {
			if ($file[0] == '.') continue;
			
			if ($file == 'README.txt') {
				$this->lastupdate = max($this->lastupdate, filectime($this->dir.$file)); // don't count README updates
				
				$this->longdesc = file_get_contents($this->dir.'README.txt');
				$matches = array();
				if (preg_match('~Written in ([^\\r\\n]+)~', $this->longdesc, $matches)) {
					$this->subtag = rtrim($matches[1], ' .');
				}
				
				if (preg_match('~Tags: ([^\\r\\n]+)~', $this->longdesc, $matches)) {
					$this->tags = array_map('trim', explode(',', $matches[1]));
				}
				
				$parts = explode("\n", $this->longdesc);
				$this->shortdesc = array_shift($parts);
				$this->shortdesc[0] = strtolower($this->shortdesc[0]); // cosmetic lowercase
				continue;
			}
						
			$this->lastupdate = max(
				$this->lastupdate,
				// filectime($this->dir.$file),
				filemtime($this->dir.$file)				
			);
						
			if (is_image($file)) {
				$this->images[] = $file;
			} else {
				$this->downloads[] = $file;
			}
		}
		
		natcasesort($this->downloads);
		$this->downloads = array_reverse($this->downloads);
		
		for($i = 0, $e = count($this->downloads); $i !== $e; ++$i) {
			$this->downloads_hashes[] = (
				sha1_file($this->dir.$this->downloads[$i])
			);
		}
	}
	
	public function genHomeImage() {		
		if (count($this->images)) {
			
			$this->homeimage = mkthumbnail(
				$this->dir.$this->images[0],
				null, // raw handle
				INDEX_THUMB_W, INDEX_THUMB_H
			);
		}
		
	}
	
	public function write() {
		
		// Generate image thumbnails
		
		foreach($this->images as $idx => $image) {
			$outfile = BASEDIR.'wwwroot/srv/'.$this->projname.'_'.$idx;
			copy($this->dir.$image, $outfile.'.'.str_ext($image));
			
			mkthumbnail($outfile.'.'.str_ext($image), $outfile.'_thumb.jpg', PAGE_THUMB_W, PAGE_THUMB_H);
		}
		
		// Copy downloads to wwwroot
		
		foreach($this->downloads as $idx => $filename) {
			$cmkdir = @mkdir( BASEDIR.'wwwroot/srv/'.$this->downloads_hashes[$idx] );
			
			if (! $cmkdir) {
				fputs(
					STDOUT,
					"WARNING: Couldn't create directory ".$this->downloads_hashes[$idx].
					" for file '${filename}'".
					" in project '".$this->projname."'!\n"
				);
			}
			
			copy(
				$this->dir.$filename, 
				BASEDIR.'wwwroot/srv/'.$this->downloads_hashes[$idx].'/'.$filename
			);
		}
		
		// Generate index page
		
		ob_start();
		$this->index();
		$idxfile = template($this->projname.' | '.SITE_TITLE, ob_get_clean());
		file_put_contents(BASEDIR.'wwwroot/'.$this->projname.'.html', $idxfile);
	}
	
	public function getClassAttr() {
		if (count($this->tags)) {
			return 'taggedWith-'.implode(' taggedWith-', $this->tags);
		} else {
			return '';
		}
	}
	
	public function index() {
?> 
<h2><?=hesc($this->projname)?></h2>

<div class="projinfo">

	<div class="projbody projbody_<?=(count($this->images) ? 'half' : 'full')?>w">
		
		<strong>ABOUT</strong>
		
		<p><?=text2html($this->longdesc)?></p>

		<?=file_get_contents(BASEDIR.'/footer.htm')?> 
		
<?php if (count($this->downloads)) { ?> 
		
		<strong>DOWNLOAD</strong>
		
		<ul>
	<?php foreach($this->downloads as $idx => $filename) { ?> 
			<li>
				<a href="srv/<?=hesc($this->downloads_hashes[$idx])?>/<?=hesc(rawurlencode($filename))?>"><?=hesc($filename)?></a>
				<small>
					<?=hesc(fbytes(filesize(BASEDIR.'wwwroot/srv/'.$this->downloads_hashes[$idx].'/'.$filename)))?>
				</small>
			</li>
	<?php } ?> 
		</ul>
<?php } ?> 
	</div>

<?php if (count($this->images)) { ?> 
	<div class="projimg">
	<?php foreach($this->images as $idx => $origname) { ?> 
		<a href="srv/<?=hesc(urlencode($this->projname))?>_<?=$idx?>.<?=str_ext($origname)?>"><img src="srv/<?=hesc(urlencode($this->projname))?>_<?=$idx?>_thumb.jpg" class="thumbimage"></a>
	<?php } ?> 
	</div>
	
	<div style="clear:both;"></div>
	
<?php } ?> 
	
</div>

<?php
	}
	
}

function template($title, $content) {
	ob_start();
?>
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" >
		<meta name="viewport" content="width=768px" >
		<link type="text/css" rel="stylesheet" href="normalize.css">
		<link type="text/css" rel="stylesheet" href="style.css">
		<script type="text/javascript" src="site.js"></script>
		<title><?=hesc($title)?></title>
	</head>
	<body>
		<div id="container">
			<div id="content">
<?=file_get_contents(BASEDIR.'/header.htm')?> 
<?=$content?> 
			</div>
		</div>
	</body>
</html>
<?php
	return ob_get_clean();
}

function listprojects() {
	// List projects

	$ls = scandir(BASEDIR.'data');
	rsort($ls);
	$projects = array();
	foreach($ls as $dirname) {
		if ($dirname[0] == '.') continue;
		$matches = array();

		if (preg_match('~(?:\d+-)?(.+)~', $dirname, $matches)) {
			$projects[$dirname] = $matches[1];
		}
	}
	
	return $projects;
}

function buildprojects($id, $projects) {
	$count = 0;
	
	foreach($projects as $dirname => $projectname) {

		echo sprintf("@%1d [%3d/%3d] ".$projectname."...\n", $id, ++$count, count($projects));

		$pr = new CProject($dirname, $projectname);	
		$pr->write();
	}
}

function buildcommon() {
	
	echo "@0 [  0/  ?] Common files...\n";
	
	$projects = listprojects();

	// Build all projects

	$plist = array();

	$handles = array();
	$handle_lookup = array();
	
	$alphasort = [];
	
	foreach($projects as $dirname => $projectname) {

		$pr = new CProject($dirname, $projectname);
		$pr->genHomeImage(); // thumbnail
		
		$plist[] = $pr;

		if (is_null($pr->homeimage)) {
			$handle_lookup[$projectname] = null;
		} else {
			$handle_lookup[$projectname] = count($handles);
			$handles[] = $pr->homeimage;
		}
		
		$alphasort[] = [$pr->projname, count($plist)-1];
	}
	
	usort($alphasort, function($a, $b) {
		return strcasecmp($a[0], $b[0]);
	});
	
	$alphaidx = [];
	
	foreach($alphasort as $a) {
		$alphaidx[ $a[1] ] = count($alphaidx);
	}

	// Build homepage spritesheet
	
	if (count($handles)) {
		mkspritesheet($handles, BASEDIR.'wwwroot/logos.jpg', INDEX_THUMB_W, INDEX_THUMB_H);
		array_map('imagedestroy', $handles); // free
	}
	
	// Build index page

	ob_start();
?>

	<?php if (file_exists(BASEDIR.'homepage_blurb.htm')) { ?> 
		<!-- homepage blurb {{ -->
		<?=file_get_contents(BASEDIR.'homepage_blurb.htm')?> 
		<!-- }} --> 
	<?php } ?> 
		
		<table id="projtable-main" class="projtable">
	<?php foreach ($plist as $i => $pr) { ?> 
			<tr class="<?=$pr->getClassAttr()?>"
				data-sort-mt="-<?=$pr->lastupdate?>"
				data-sort-ct="<?=$i?>"
				data-sort-al="<?=$alphaidx[$i]?>"
			>
				<td>
					<a href="<?=hesc(urlencode($pr->projname))?>.html"><?=(is_null($handle_lookup[$pr->projname]) ? '<div class="no-image"></div>' : '<div class="homeimage homeimage-sprite" style="background-position:0 -'.($handle_lookup[$pr->projname]*INDEX_THUMB_H).'px"></div>')?></a>
				</td>
				<td>
					<strong><?=hesc($pr->projname)?></strong>,
					<?=hesc($pr->shortdesc)?> 
					<a href="<?=hesc(urlencode($pr->projname))?>.html">more...</a>
		<?php if (strlen($pr->subtag) || count($pr->tags)) { ?> 
					<br>
					<small>
						<?=hesc($pr->subtag)?> 
			<?php if (strlen($pr->subtag) && count($pr->tags)) { ?> 
						::
			<?php } ?> 
			<?php foreach($pr->tags as $tag) { ?> 
						<a class="tag tag-link" data-tag="<?=hesc($tag)?>"><?=hesc($tag)?></a>
			<?php } ?> 
					</small>
		<?php } ?> 
				</td>
			</tr>
	<?php } ?> 
		</table>
<?php

	$index = template(SITE_TITLE, ob_get_clean());
	file_put_contents(BASEDIR.'wwwroot/index.html', $index);
	
	// Done
}

function buildredirects($redirects) {
	foreach($redirects as $oldname => $newname) {
		ob_start();
?>
<meta http-equiv="refresh" content="0; url=<?=hesc($newname)?>.html">
<a href="<?=hesc($newname)?>.html">Moved &raquo;</a>
<?php
		$page = ob_get_clean();
		file_put_contents(BASEDIR.'wwwroot/'.$oldname.'.html', $page);
	}
}

function main($args) {
	$basedir = './';
	$total   = $args[0];
	$pos     = $args[1];
	
	// Parse configuration
	
	$config = @parse_ini_file(
		$basedir . 'config.ini',
		true,
		INI_SCANNER_RAW
	);
	
	if ($config === false) {
		die("[FATAL] Couldn't load '${basedir}/config.ini'!\n");
	}
	
	define('BASEDIR',       $basedir);
	define('SITE_TITLE',    trim($config['codesite']['title']));
	define('PAGE_THUMB_W',  intval($config['codesite']['page_thumb_w']));
	define('PAGE_THUMB_H',  intval($config['codesite']['page_thumb_h']));
	define('INDEX_THUMB_W', intval($config['codesite']['index_thumb_w']));
	define('INDEX_THUMB_H', intval($config['codesite']['index_thumb_h']));
	
	// Perform build tasks
	
	if ($pos == 0) {
		buildcommon();
		if (array_key_exists('redirect', $config)) {
			buildredirects( $config['redirect'] );
		}
	} else {
		buildprojects($pos, array_decimate(listprojects(), $total, $pos));
	}
}

// Entry point
//

ini_set('display_errors', 'On');
error_reporting(E_ALL);

main(array_slice($_SERVER['argv'], 1));
