//----------------------------------------------------------------------
//  This software is part of the OpenBeOS distribution and is covered 
//  by the OpenBeOS license.
//---------------------------------------------------------------------
/*!
	\file AssociatedTypes.cpp
	AssociatedTypes class implementation
*/

#include "mime/AssociatedTypes.h"

#include <Directory.h>
#include <Entry.h>
#include <Message.h>
#include <MimeType.h>
#include <Path.h>
#include <String.h>
#include <kernel_interface.h>
#include <mime/database_support.h>
#include <storage_support.h>

#include <new>
#include <stdio.h>

#define DBG(x) x
//#define DBG(x)
#define OUT printf

namespace BPrivate {
namespace Storage {
namespace Mime {

/*!
	\class AssociatedTypes
	\brief Information about file extensions and their associated types
*/

// Constructor
//! Constructs a new AssociatedTypes object
AssociatedTypes::AssociatedTypes()
	: fHaveDoneFullBuild(false)
{
}

// Destructor
//! Destroys the AssociatedTypes object
AssociatedTypes::~AssociatedTypes()
{
}

// GetAssociatedTypes
/*! \brief Returns a list of mime types associated with the given file
	extension in the pre-allocated \c BMessage pointed to by \c types.
	
	See \c BMimeType::GetAssociatedTypes() for more information.
*/
status_t 
AssociatedTypes::GetAssociatedTypes(const char *extension, BMessage *types)
{
	status_t err = extension && types ? B_OK : B_BAD_VALUE;
	std::string extStr;
	
	// See if we need to do our initial build still
	if (!err && !fHaveDoneFullBuild) {
		err = BuildAssociatedTypesTable();
	}
	// Format the extension 
	if (!err) {
		extStr = PrepExtension(extension);		
		err = extStr.length() > 0 ? B_OK : B_BAD_VALUE;
	}
	// Build the message
	if (!err) {
		// Clear the message, as we're just going to add to it
		types->MakeEmpty();
		
		// Add the types associated with this extension
		std::set<std::string> &assTypes = fAssociatedTypes[extStr];
		std::set<std::string>::const_iterator i;
		for (i = assTypes.begin(); i != assTypes.end() && !err; i++) {
			err = types->AddString(kTypesField, i->c_str());
		}
	}
	return err;
}

// GuessMimeType
/*! \brief Guesses a MIME type for the given filename based on its extension

	\param filename The filename of interest
	\param result Pointer to a pre-allocated \c BString object into which
	              the result is stored. If the function returns a value other
	              than \c B_OK, \a result will not be modified.
	\return
	- \c B_OK: success
	- \c other error code: failure	
*/
status_t
AssociatedTypes::GuessMimeType(const char *filename, BString *result)
{
	status_t err = filename && result ? B_OK : B_BAD_VALUE;
	if (!err && !fHaveDoneFullBuild)
		err = BuildAssociatedTypesTable();
	if (!err) {
		// Extract the extension from the file
		uint i = strlen(filename);
		while (i-1 >= 0 && filename[i-1] != '.')
			i--;
		
		// If there was an extension, grab it and look up its associated
		// type(s). Otherwise, the best guess we can offer is
		// "application/octect-stream"
		if (i > 0) {
			std::string extension = PrepExtension(&(filename[i]));
			
			/*! \todo I'm just grabbing the first item in the set here. Should we perhaps
				do something different?
			*/
			std::set<std::string> &types = fAssociatedTypes[extension];
			std::set<std::string>::const_iterator i = types.begin();
			if (i != types.end())
				result->SetTo(i->c_str());
			else
				err = kMimeGuessFailureError;
		} else {
			err = kMimeGuessFailureError;
		}
	}
	return err;		
}

// GuessMimeType
/*! \brief Guesses a MIME type for the given \c entry_ref based on its filename extension

	\param filename The entry_ref of interest
	\param result Pointer to a pre-allocated \c BString object into which
	              the result is stored. If the function returns a value other
	              than \c B_OK, \a result will not be modified.
	\return
	- \c B_OK: success
	- \c other error code: failure	
*/
status_t
AssociatedTypes::GuessMimeType(const entry_ref *ref, BString *result)
{
	// Convert the entry_ref to a filename and then do the check
	/*! \todo If the ref is invalid, */
	char path[B_PATH_NAME_LENGTH];
	status_t err = entry_ref_to_path(ref, path, B_PATH_NAME_LENGTH);
	if (!err)
		err = GuessMimeType(path, result);
	return err;
}

// SetFileExtensions
/*! \brief Sets the list of file extensions for the given type and
	updates the associated types mappings.
	
	All listed extensions will including the given mime type in
	their list of associated types following this call.
	
	All extensions previously but no longer associated with this
	mime type will no longer list this mime type as an associated
	type.
	
	\param app The mime type whose associated file extensions you are setting
	\param types Pointer to a \c BMessage containing an array of associated
	             file extensions in its \c Mime::kExtensionsField field.	             
*/
status_t
AssociatedTypes::SetFileExtensions(const char *type, const BMessage *extensions)
{
	status_t err = type && extensions ? B_OK : B_BAD_VALUE;
	if (!fHaveDoneFullBuild)
		return err;
		
	std::set<std::string> oldExtensions;
	std::set<std::string> &newExtensions = fFileExtensions[type];
	// Make a copy of the previous extensions
	if (!err)
		oldExtensions = newExtensions;

	if (!err) {
		// Read through the list of new extensions, creating the new
		// file extensions list and adding the type as an associated type
		// for each extension
		newExtensions.clear();
		const char *extension;
		for (int32 i = 0;
			   extensions->FindString(kTypesField, i, &extension) == B_OK;
			     i++)
		{
			newExtensions.insert(extension);
			AddAssociatedType(extension, type);
		}
		
		// Remove any extensions that are still associated from the list
		// of previously associated extensions
		for (std::set<std::string>::const_iterator i = newExtensions.begin();
			   i != newExtensions.end();
			     i++)
		{
			oldExtensions.erase(*i);
		}
				
		// Now remove the type as an associated type for any of its previously
		// but no longer associated extensions
		for (std::set<std::string>::const_iterator i = oldExtensions.begin();
			   i != oldExtensions.end();
			     i++)
		{
			RemoveAssociatedType(i->c_str(), type);
		}
	}	
	return err;
}

// DeleteFileExtensions
/*! \brief Clears the given types's file extensions list and removes the
	types from each of said extensions' associated types list.
	\param app The mime type whose file extensions you are clearing
*/
status_t
AssociatedTypes::DeleteFileExtensions(const char *type)
{
	BMessage extensions;
	return SetFileExtensions(type, &extensions);
}

// PrintToStream
//! Dumps the associated types mapping to standard output
void
AssociatedTypes::PrintToStream() const
{
	printf("\n");
	printf("-----------------\n");
	printf("Associated Types:\n");
	printf("-----------------\n");
	
	for (std::map<std::string, std::set<std::string> >::const_iterator i = fAssociatedTypes.begin();
		   i != fAssociatedTypes.end();
		     i++)
	{
		printf("%s: ", i->first.c_str());
		fflush(stdout);
		bool first = true;
		for (std::set<std::string>::const_iterator type = i->second.begin();
			   type != i->second.end();
			     type++)
		{
			if (first)
				first = false;
			else
				printf(", ");
			printf("%s", type->c_str());
			fflush(stdout);
		}
		printf("\n");
	}
}

// AddAssociatedType
/*! \brief Adds the given mime type to the set of associated types
	for the given extension.
	
	\param extension The file extension
	\param type The associated mime type
	\return
	- B_OK: success, even if the type was already in the associated types list
	- "error code": failure
*/
status_t
AssociatedTypes::AddAssociatedType(const char *extension, const char *type)
{
	status_t err = extension && type ? B_OK : B_BAD_VALUE;
	std::string extStr;
	if (!err) {
		extStr = PrepExtension(extension);
		err = extStr.length() > 0 ? B_OK : B_BAD_VALUE;
	}
	if (!err) 
		fAssociatedTypes[extStr].insert(type);
	return err;
}

// RemoveAssociatedType
/*! \brief Removes the given mime type from the set of associated types
	for the given extension.
	
	\param extension The file extension
	\param type The associated mime type
	\return
	- B_OK: success, even if the type was not found in the associated types list
	- "error code": failure
*/
status_t
AssociatedTypes::RemoveAssociatedType(const char *extension, const char *type)
{
	status_t err = extension && type ? B_OK : B_BAD_VALUE;
	std::string extStr;
	if (!err) {
		extStr = PrepExtension(extension);
		err = extStr.length() > 0 ? B_OK : B_BAD_VALUE;
	}
	if (!err) 
		fAssociatedTypes[extension].erase(type);
	return err;
}

// BuildAssociatedTypesTable
/*! \brief Crawls the mime database and builds a list of associated types
	for every associated file extension.
*/
status_t
AssociatedTypes::BuildAssociatedTypesTable()
{
	fFileExtensions.clear();
	fAssociatedTypes.clear();
	
	BDirectory root;
	status_t err = root.SetTo(kDatabaseDir.c_str());
	if (!err) {
		root.Rewind();
		while (true) {		
			BEntry entry;
			err = root.GetNextEntry(&entry);
			if (err) {
				// If we've come to the end of list, it's not an error
				if (err == B_ENTRY_NOT_FOUND) 
					err = B_OK;
				break;
			} else {
				// Check that this entry is both a directory and a valid MIME string
				char supertype[B_PATH_NAME_LENGTH];	
				if (entry.IsDirectory()
				      && entry.GetName(supertype) == B_OK
				         && BMimeType::IsValid(supertype))
				{
					// Make sure the supertype string is all lowercase
					BPrivate::Storage::to_lower(supertype);
					
					// First, iterate through this supertype directory and process
					// all of its subtypes
					BDirectory dir;
					if (dir.SetTo(&entry) == B_OK) {
						dir.Rewind();
						while (true) {
							BEntry subEntry;						
							err = dir.GetNextEntry(&subEntry);
							if (err) {
								// If we've come to the end of list, it's not an error
								if (err == B_ENTRY_NOT_FOUND) 
									err = B_OK;
								break;
							} else {																	
								// Get the subtype's name
								char subtype[B_PATH_NAME_LENGTH];
								if (subEntry.GetName(subtype) == B_OK) {
									BPrivate::Storage::to_lower(subtype);
									
									char fulltype[B_PATH_NAME_LENGTH];
									sprintf(fulltype, "%s/%s", supertype, subtype);
								
									// Process the subtype
									ProcessType(fulltype);
								}
							}	
						}
					} else {
						DBG(OUT("Mime::AssociatedTypes::BuildAssociatedTypesTable(): "
						          "Failed opening supertype directory '%s'\n",
						            supertype));
					}
					
					// Second, process the supertype
					ProcessType(supertype);									
				} 
			}			
		}			
	} else {
		DBG(OUT("Mime::AssociatedTypes::BuildAssociatedTypesTable(): "
		          "Failed opening mime database directory '%s'\n",
		            kDatabaseDir.c_str()));
	}
	if (!err) {
		fHaveDoneFullBuild = true;
//		PrintToStream();
	}
	else
		DBG(OUT("Mime::AssociatedTypes::BuildAssociatedTypesTable() failed, error code == 0x%lx\n", err));
	return err;
	
}

// ProcessType
/*! \brief Handles a portion of the initial associated types table construction for
	the given mime type.
	
	\note To be called by BuildAssociatedTypesTable() *ONLY*. :-)
	
	\param type The mime type of interest. The mime string is expected to be valid
	            and lowercase. Both "supertype" and "supertype/subtype" mime types
	            are allowed.
*/
status_t
AssociatedTypes::ProcessType(const char *type)
{
	status_t err = type ? B_OK : B_BAD_VALUE;
	if (!err) {
		// Read in the list of file extension types
		BMessage msg;
		if (read_mime_attr_message(type, kFileExtensionsAttr, &msg) == B_OK) {
			// Iterate through the file extesions, adding them to the list of
			// file extensions for the mime type and adding the mime type
			// to the list of associated types for each file extension
			const char *extension;
			std::set<std::string> &fileExtensions = fFileExtensions[type];
			for (int i = 0; msg.FindString(kExtensionsField, i, &extension) == B_OK; i++) {
				std::string extStr = PrepExtension(extension);
				if (extStr.length() > 0) {		
					fileExtensions.insert(extStr);
					AddAssociatedType(extStr.c_str(), type);
				}
			}
		}
	}
	return err;
}

// PrepExtension
/*! \brief Strips any leading '.' chars from the given extension and
	forces all chars to lowercase.
*/
std::string
AssociatedTypes::PrepExtension(const char *extension) const
{
	if (extension) {
		uint i = 0;
		while (extension[i] == '.')
			i++;
		return BPrivate::Storage::to_lower(&(extension[i]));
	} else {
		return "";	// This shouldn't ever happen, but if it does, an
		            // empty string is considered an invalid extension
	}
}

} // namespace Mime
} // namespace Storage
} // namespace BPrivate

