<?php


final class LoaderException extends Exception { }


final class ConfigGetter
{
	private $config;
	private $package;

	public function __construct( Package $package , $config )
	{
		$this->package = $package;
		$this->config = $config;
	}


	public function get( $path = '' , $default = null , $fail = false )
	{
		if ( $path == '' ) {
			return $this->config;
		}

		$aPath = explode( '/' , $path );
		$config = &$this->config;
		foreach ( $aPath as $name ) {
			if ( !( is_array( $config ) && array_key_exists( $name , $config ) ) ) {
				if ( $fail ) {
					throw new LoaderException( "configuration key '$path' not found for package '"
						. $this->package->name() . "'" );
				}
				return $default;
			}
			$config = &$config[ $name ];
		}
		return $config;
	}
}


final class Package
{
	private $name;

	private $files;
	private $requires;

	private $ctrls;
	private $daos;
	private $extras;
	private $hooks;
	private $pages;
	private $singletons;
	private $views;

	private $loaded = false;
	private $config;

	public function __construct( $name , $description , $config )
	{
		$this->name = $name;

		$fields = array( 'files' , 'requires' , 'daos' , 'views' , 'ctrls' , 'extras' , 'singletons' , 'pages' , 'hooks' );
		foreach ( $fields as $field ) {
			$this->getField( $description , $field );
		}
		if ( empty( $this->files ) ) {
			throw new LoaderException( "package '{$this->name}': no files" );
		}

		if ( ! is_array( $config ) ) {
			$config = array( );
		}
		$this->config = new ConfigGetter( $this , $config );
	}


	private function getField( $description , $field )
	{
		if ( ! array_key_exists( $field , $description ) ) {
			$this->$field = array( );
			return;
		}

		$value = $description[ $field ];
		if ( !is_array( $value ) ) {
			throw new LoaderException( "package '{$this->name}': '$field' must be an array" );
		}

		foreach ( $value as $item ) {
			if ( !is_string( $item ) ) {
				throw new LoaderException( "package '{$this->name}': '$field' contains non-string items" );
			}
		}

		$this->$field = $value;
	}


	public function name( )
	{
		return $this->name;
	}

	public function files( )
	{
		return $this->files;
	}

	public function requires( )
	{
		return $this->requires;
	}

	public function daos( )
	{
		return $this->daos;
	}

	public function views( )
	{
		return $this->views;
	}

	public function ctrls( )
	{
		return $this->ctrls;
	}

	public function singletons( )
	{
		return $this->singletons;
	}

	public function extras( )
	{
		return $this->extras;
	}

	public function pages( )
	{
		return $this->pages;
	}


	public function loaded( )
	{
		return $this->loaded;
	}

	public function hooks( )
	{
		return $this->hooks;
	}

	public function setLoaded( )
	{
		$this->loaded = true;
	}


	public function config( $path = '' , $default = null , $fail = false )
	{
		return $this->config->get( $path , $default , $fail );
	}

	public function getConfigAccess( )
	{
		return $this->config;
	}
}


interface PackageAware
{
	public function setPackage( Package $package );
}


interface TextSource
{
	public function get( $what );
}


final class Loader
{
	private static $loader = null;

	private $baseDir;
	private $config;
	private $packages = array( );
	private $items = array(
			'ctrls'		=> array( ) ,
			'daos'		=> array( ) ,
			'extras'	=> array( ) ,
			'singletons'	=> array( ) ,
			'views'		=> array( ) ,
			'pages'		=> array( ) ,
		);
	private $loading = array( );
	private $singletons = array( );
	private $daos = array( );
	private $textSource;

	private function __construct( )
	{
		$this->baseDir = dirname( __FILE__ );
		$this->loadConfig( );
		$this->loadPackageDescriptions( );
	}

	private function loadConfig( )
	{
		$config = array( );
		@include( $this->baseDir . '/config.inc.php' );
		$this->config = $config;
	}

	private function loadPackageDescriptions( )
	{
		if ( !( $dh = opendir( $this->baseDir ) ) ) {
			throw new LoaderException( "unable to access directory" );
		}

		while ( ( $entry = readdir( $dh ) ) !== false ) {
			if ( $entry === '.' || $entry === '..' ) {
				continue;
			}

			$path = "{$this->baseDir}/$entry";
			if ( is_dir( $path ) && is_file( "$path/package.inc.php" ) ) {
				$this->loadDescription( $entry );
			}
		}

		closedir( $dh );
	}


	private function loadDescription( $name )
	{
		$package = array( );
		require( $this->baseDir . '/' . $name . '/package.inc.php' );
		if ( empty( $package ) ) {
			throw new LoaderException( "package '$name': no information" );
		}

		if ( ! array_key_exists( $name , $this->config ) ) {
			$this->config[ $name ] = array( );
		}

		$package = new Package( $name , $package , $this->config[ $name ] );
		$this->packages[ $name ] = $package;
		$this->config[ $name ] = null;

		foreach ( array_keys( $this->items ) as $type ) {
			$items = $package->$type( );
			foreach ( $items as $item ) {
				if ( array_key_exists( $item , $this->items[ $type ] ) ) {
					$oName = $this->items[ $type ][ $item ];
					$type = substr( $type , 0 , strlen( $type ) - 1 );
					throw new LoaderException( "package '$name': conflict with '$oName' on $type '$item'" );
				}
				$this->items[ $type ][ $item ] = $name;
			}
		}
	}


	private function loadPackage( $name )
	{
		if ( ! array_key_exists( $name , $this->packages ) ) {
			throw new LoaderException( "Package '$name' not found" );
		}

		$package = $this->packages[ $name ];
		if ( $package->loaded( ) ) {
			return;
		}

		if ( array_key_exists( $name , $this->loading ) ) {
			throw new LoaderException( "Package '$name': recursive dependencies detected" );
		}
		$this->loading[ $name ] = 1;

		foreach ( $package->requires( ) as $dependency ) {
			$this->loadPackage( $dependency );
		}

		$dir = $this->baseDir . '/' . $name;
		foreach ( $package->files( ) as $file ) {
			require_once( "$dir/$file.inc.php" );
		}

		unset( $this->loading[ $name ] );
		$package->setLoaded( );

		$hooks = $package->hooks( );
		if ( is_array( $hooks ) ) {
			foreach ( $hooks as $hook ) {
				$hook( $this , $package );
			}
		}
	}


	private function findItem( $name , $type )
	{
		$rType = $type . 's';
		if ( ! array_key_exists( $rType , $this->items ) ) {
			throw new LoaderException( "Invalid type '$type'" );
		}

		if ( ! array_key_exists( $name , $this->items[ $rType ] ) ) {
			throw new LoaderException( "Item '$name' of type $type not found" );
		}

		$package = $this->items[ $rType ][ $name ];
		$this->loadPackage( $package );
		return $this->packages[ $package ];
	}


	private function createInstance( $package , $cName , $args )
	{
		if ( empty( $args ) ) {
			$instance = new $cName();
		} else {
			$reflection = new ReflectionClass( $cName );
			$instance = $reflection->newInstanceArgs( $args );
		}

		if ( is_a( $instance , 'PackageAware' ) ) {
			$instance->setPackage( $package );
		}
		return $instance;
	}


	private function loadAndCreate( $type , $name , $cName , $args )
	{
		$package = $this->findItem( $name , $type );
		return $this->createInstance( $package , $cName , $args ); 
	}


	private function getSingleton( $name )
	{
		if ( ! array_key_exists( $name , $this->singletons ) ) {
			$this->singletons[ $name ] = $this->loadAndCreate( 'singleton' , $name , $name , array( ) );
		}
		return $this->singletons[ $name ];
	}


	private function getDao( $name )
	{
		if ( array_key_exists( $name , $this->daos ) ) {
			return $this->daos[ $name ];
		}
		$cName = Loader::convertName( 'DAO' , $name );
		$instance = $this->loadAndCreate( 'dao' , $name , $cName , array( ) );
		$instance->setDatabase( Loader::Singleton( 'Database' ) );
		$this->daos[ $name ] = $instance;
		return $instance;
	}


	private static function get( )
	{
		if ( Loader::$loader === null ) {
			Loader::$loader = new Loader( );
		}
		return Loader::$loader;
	}

	private static function convertName( $type , $name )
	{
		$cName = ucfirst( $type ) . '_';
		foreach ( explode( '_' , $name ) as $part ) {
			$cName .= ucfirst( $part );
		}
		return $cName;
	}

	private static function creator( $type , $convert , $args )
	{
		$name = array_shift( $args );
		$cName = $convert ? Loader::convertName( $type , $name ) : $name;
		return Loader::get( )->loadAndCreate( $type , $name , $cName , $args );
	}


	public static function PackageConfig( $name )
	{
		$loader = Loader::get( );
		$loader->loadPackage( $name );
		return $loader->packages[ $name ]->getConfigAccess( );
	}


	public static function TextSource( TextSource $source = null )
	{
		if ( $source !== null ) {
			Loader::get( )->textSource = $source;
		} else {
			return Loader::get( )->textSource;
		}
	}


	public static function Text( $what )
	{
		$source = Loader::get( )->textSource;
		return $source ? $source->get( $what ) : $what;
	}


	public static function Load( $name , $type = 'extra' )
	{
		Loader::get( )->findItem( $name , $type );
	}


	public static function Singleton( $name )
	{
		return Loader::get( )->getSingleton( $name );
	}

	public static function Create( )
	{
		return Loader::creator( 'extra' , false , func_get_args( ) );
	}

	public static function View( )
	{
		return Loader::creator( 'view' , true , func_get_args( ) );
	}

	public static function Ctrl( )
	{
		return Loader::creator( 'ctrl' , true , func_get_args( ) );
	}

	public static function Page( )
	{
		return Loader::creator( 'page' , true , func_get_args( ) );
	}

	public static function DAO( $name )
	{
		return Loader::get( )->getDao( $name );
	}
}