/*
 * \brief  ISO 9660 file system support
 * \author Sebastian Sumpf <sebastian.sumpf@genode-labs.com>
 * \date   2010-07-26
 */

/*
 * Copyright (C) 2010-2012 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU General Public License version 2.
 */

/* Genode includes */
#include <base/allocator_avl.h>
#include <base/exception.h>
#include <base/printf.h>
#include <base/stdint.h>
#include <block_session/connection.h>
#include <util/misc_math.h>
#include <util/token.h>

#include "iso9660.h"

using namespace Genode;

namespace Iso {

	/*
	 *  Sector reads one or more packets from the block interface
	 */
	class Sector {

		public:

			enum {
				MAX_SECTORS = 32, /* max. number sectors that can be read in one
				                     transaction */
			};

			static Block::Connection          *_blk;
			static Block::Session::Tx::Source *_source;
			static size_t                      _blk_size;
			static Lock                        _lock;

		private:

			Block::Packet_descriptor _p;

		public:

			Sector(unsigned long blk_nr, unsigned long count)
			{
				Lock::Guard lock_guard(_lock);

				try {
					_p = Block::Packet_descriptor(
						_blk->dma_alloc_packet(blk_size() * count),
						Block::Packet_descriptor::READ,
						blk_nr * ((float)blk_size() / _blk_size),
						count * ((float)blk_size() / _blk_size));
					_source->submit_packet(_p);
					_p = _source->get_acked_packet();

					if (!_p.succeeded()) {
						PERR("Could not read block %lu", blk_nr);
						throw Io_error();
					}

				} catch (Block::Session::Tx::Source::Packet_alloc_failed) {
					PERR("Packet overrun!");
					_p = _source->get_acked_packet();
					throw Io_error();
				}
			}

			~Sector() { 
				Lock::Guard lock_guard(_lock);
				_source->release_packet(_p);
			}

			/**
			 * Return address of packet content
			 */
			template <typename T>
			T addr() { return reinterpret_cast<T>(_source->packet_content(_p)); }

			static size_t blk_size() { return 2048; }

			static unsigned long to_blk(unsigned long bytes) {
				return ((bytes + blk_size() - 1) & ~(blk_size() - 1)) / blk_size(); }
	};

	/**
	 * Rock ridge extension (see IEEE P1282)
	 */
	class Rock_ridge
	{
		public:

			enum {
				NM = 0x4d4e, /* POSIX name system use entry (little endian) */
			};

		private:

			uint16_t _signature;
			uint8_t  _length;
			uint8_t  _version;
			uint8_t  _flags;
			char _name;

		public:

			char   *name()   { return &_name; }
			uint8_t length() { return _length - 5; }

			static Rock_ridge* scan_name(uint8_t *ptr, uint8_t size)
			{
				Rock_ridge *rr = (Rock_ridge *)ptr;

				while (rr->_length && ((uint8_t *)rr < ptr + size - 4)) {
					if (rr->_signature == NM)
						return rr;
					rr = rr->next();
				}

				return 0;
			}

			Rock_ridge *next() { return (Rock_ridge *)((uint8_t *)this + _length); }
	};


	/*********************
	 ** ISO9660 classes **
	 *********************/

	/**
	 * Access memory using offsets
	 */
	class Iso_base
	{
		protected:

			template <typename T>
			T value(int offset) { return *((T *)(this + offset)); }

			template <typename T>
			T ptr(int offset) { return (T)(this + offset); }
	};


	/**
	 * Class representing a directory descriptions (see ECMA 119)
	 */
	class Directory_record : public Iso_base
	{
		enum {
			TABLE_LENGTH = 33,  /* fixed length of directory record */

			ROOT_DIR     = 0x0, /* special names of root and parent directories */
			PARENT_DIR   = 0x1,

			DIR_FLAG     = 0x2, /* directory flag in attributes */
		};

		public:

			/* total length of this record */
			uint8_t  record_length() { return value<uint8_t>(0); }

			/* starting block of extent */
			uint32_t blk_nr() { return value<uint32_t>(2); }

			/* length in bytes of extent */
			uint32_t data_length() { return value<uint32_t>(10); }

			/* attributes */
			uint8_t  file_flags() { return value<uint8_t>(25); }

			/* length of file name */
			uint8_t   file_name_length() { return value<uint8_t>(32); }

			/* retrieve the file name */
			void file_name(char *buf)
			{
				buf[0] = 0;

				/* try Rock Ridge name */
				Rock_ridge *rr = Rock_ridge::scan_name(system_use(),
				                                       system_use_size());

				if (rr) {
					memcpy(buf, rr->name(), rr->length());
					buf[rr->length()] = 0;
					return;
				}

				/* retrieve iso name */
				char *name = ptr<char*>(33);

				/*
				 * Check for root and parent directory names and modify them
				 * to '.' and '..' respectively.
				 */
				if (file_name_length() == 1)

					switch (name[0]) {

					case PARENT_DIR:

						buf[2] = 0;
						buf[1] = '.';
						buf[0] = '.';
						return;

					case ROOT_DIR:

						buf[1] = 0;
						buf[0] = '.';
						return;
					}

				memcpy(buf, name, file_name_length());
				buf[file_name_length()] = 0;
			}

			/* pad byte after file name (if file name length is even, only) */
			uint8_t pad_byte() { return !(file_name_length() % 2) ? 1 : 0; }

			/* system use area */
			uint8_t *system_use()
			{
				return (uint8_t*)this + TABLE_LENGTH
				        + file_name_length() + pad_byte();
			}

			/* length in bytes of system use area */
			uint8_t system_use_size()
			{
				return record_length() - file_name_length()
				       - TABLE_LENGTH - pad_byte();
			}

			/* retrieve next record */
			Directory_record *next()
			{
				Directory_record *_next = this + record_length();

				if (_next->record_length())
					return _next;

				return 0;
			}

			/* find a directory record with file name matching 'level' */
			Directory_record *locate(char *level)
			{
				Directory_record *dir = this;

				while (dir) {

					char name[LEVEL_LENGTH];
					dir->file_name(name);

					if (!strcmp(name, level))
						return dir;

					dir = dir->next();
				}

				return 0;
			}

			/* describes this record a directory */
			bool is_directory() { return file_flags() & DIR_FLAG; }
	};


	/**
	 * Volume descriptor (see ECMA 119)
	 */
	class Volume_descriptor : public Iso_base
	{
		enum {
			/* volume types */
			PRIMARY    = 0x01, /* type of primary volume descriptor */
			TERMINATOR = 0xff, /* type of terminating descriptor */

			/* constants */
			ROOT_SIZE  = 34, /* the root directory record has a fixed length */
		};

		public:

			/* descriptor type */
			uint8_t type() { return value<uint8_t>(0); }

			/* root directory record */
			Directory_record * root_record() { return ptr<Directory_record *>(156); }

			/* check for primary descriptor */
			bool is_primary() { return type() == PRIMARY; }

			/* check for terminating descriptor */
			bool is_terminator() { return type() == TERMINATOR; }

			/* copy the root record */
			Directory_record *copy_root_record()
			{
				Directory_record *buf;

				if (!(env()->heap()->alloc(ROOT_SIZE, &buf)))
					throw Root::Quota_exceeded();

				memcpy(buf, root_record(), ROOT_SIZE);

				return buf;
			}
	};


	/**
	 * Locate the root-directory record in the primary volume descriptor
	 */
	Directory_record *locate_root()
	{
		/* volume descriptors in ISO9660 start at block 16 */
		for (unsigned long blk_nr = 16;; blk_nr++) {
			Sector sec(blk_nr, 1);
			Volume_descriptor *vol = sec.addr<Volume_descriptor *>();

			if (verbose)
				PDBG("Volume: type %u", vol->type());

			if (vol->is_primary())
				return vol->copy_root_record();

			if (vol->is_terminator())
				return 0;
		}
	}


	/**
	 * Return root directory record
	 */
	Directory_record *root_dir()
	{
		static Directory_record *root = locate_root();

		if (!root)
			throw Non_data_disc();

		if (verbose)
			PDBG("Good root");
		return root;
	}


	unsigned long read_file(File_info *info, off_t file_offset, uint32_t length, void *buf_ptr)
	{
		uint8_t *buf = (uint8_t *)buf_ptr;
		if (info->size() <= (size_t)(length + file_offset))
			length = info->size() - file_offset - 1;

		unsigned long total_blk_count = ((length + (Sector::blk_size() - 1)) &
		                                 ~((Sector::blk_size()) - 1)) / Sector::blk_size();
		unsigned long ret = total_blk_count;
		unsigned long blk_count;
		unsigned long blk_nr = info->blk_nr() + (file_offset / Sector::blk_size());

		if (verbose)
			PDBG("Read blk %lu count %lu, file_offset: %08lx length %u", blk_nr, total_blk_count, file_offset, length);

		while ((blk_count = min<unsigned long>(Sector::MAX_SECTORS, total_blk_count))) {
			Sector sec(blk_nr, blk_count);

			total_blk_count -= blk_count;
			blk_nr          += blk_count;

			unsigned long copy_length = blk_count * Sector::blk_size();
			memcpy(buf, sec.addr<void *>(), copy_length);

			length -= copy_length;
			buf    += copy_length;

			/* zero out rest of page */
			if (!total_blk_count && (blk_count % 2))
				memset(buf, 0, Sector::blk_size());
		}
	
		return ret * Sector::blk_size();
	}


	struct Scanner_policy_file
	{
		static bool identifier_char(char c, unsigned /* i */)
		{
			return c != '/' && c != 0;
		}
	};

	typedef ::Genode::Token<Scanner_policy_file> Token;


	/**
	 * ISO interface
	 */
	File_info *file_info(char *path)
	{
		char level[PATH_LENGTH];

		Token t(path);
		Directory_record *dir = root_dir();
		uint32_t blk_nr = 0, data_length = 0;

		/* determine block nr and file length on disk, parse directory records */
		while (t) {

			if (t.type() != Token::IDENT) {
				t = t.next();
				continue;
			}

			t.string(level, PATH_LENGTH);

			/* load extent of directory record and search for level */
			for (unsigned long i = 0; i < Sector::to_blk(dir->data_length()); i++) {
				Sector sec(dir->blk_nr() + i, 1);
				Directory_record *tmp = sec.addr<Directory_record *>()->locate(level);

				if (!tmp && i == Sector::to_blk(dir->data_length()) - 1) {
					PERR("File not found: %s", path);
					throw File_not_found();
				}

				if (!tmp) continue;

				dir = tmp;

				if (verbose)
					PDBG("Found %s", level);

				if (!dir->is_directory()) {
					blk_nr      = dir->blk_nr();
					data_length = dir->data_length();
				}

				break;
			}

			t = t.next();
		}

		/* Warning: Don't access 'dir' after this point, since the sector is gone */

		if (!blk_nr && !data_length) {
			PERR("File not found: %s", path);
			throw File_not_found();
		}

		if (verbose)
			PDBG("Path: %s Block nr: %u Length: %u", path, blk_nr, data_length);

		return new(env()->heap()) File_info(blk_nr, data_length);
	}


	/*
	 * Static members of Sector
	 */
	Block::Connection *Sector::_blk;
	Block::Session::Tx::Source *Sector::_source;
	size_t Sector::_blk_size;
	Lock Sector::_lock;


	/**
	 * Initialize globals
	 */
	void __attribute__((constructor)) init()
	{
		static Allocator_avl block_alloc(env()->heap());
		static Block::Connection _blk(&block_alloc);

		Sector::_blk    = &_blk;
		Sector::_source  = _blk.tx();

		size_t blk_cnt = 0;
		Block::Session::Operations ops;
		_blk.info(&blk_cnt, &Sector::_blk_size, &ops);
	}
} /* end of namespace Iso */
