/*
 * $Id$
 *
 * edelib::MimeType - Detection of file types and handling
 * Part of Equinox Desktop Environment (EDE).
 * Copyright (c) 2000-2006 EDE Authors.
 *
 * This program is licenced under terms of the
 * GNU General Public Licence version 2 or newer.
 * See COPYING for details.
 */

#include "MimeType.h"

#include <fltk/filename.h>

#include "Run.h"
#include "Config.h"
#include "Util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>

#include <magic.h>

// I made this icon because on KDE there are stupidly two icons with same name
#define DEFAULT_ICON "mimetypes/misc"
#define FOLDER_ICON "folder"
#define RECYCLED_ICON "recycled"
#define LOCKED_ICON "lockoverlay"
#define FOLDERLOCKED_ICON "folder_locked"
// to be defined in separate file:
#define FILE_MANAGER "efiler"


using namespace fltk;
using namespace edelib;


// GLOBAL NOTE: asprintf() is a GNU extension - if it's unsupported on some
// platforms, use our tasprintf() instead (in Util.h)



// File format:
//  id|description|handler program|icon|wildcard for filename (extension)|wildcard for file command output|classic mime type
// 
//  - id - short string; to setup subtypes, just use slash (/) as separator in ID
//  - description - what is shown in gui
//  - handler program - filename will be appended, specify any needed parameters for opening - THIS WILL BE MOVED INTO SEPARATE FILE (for handling multiple programs etc.)
//  - icon - just name, don't give extension or path
//  - extension - will be used only if multiple types have same file command match. You don't need to give asterisk (*) i.e. .png. If there are multiple extensions, separate them with slash (/). Actually, "extension" can be any part of filename, but I can't think of use for this
//  - file output - relevant part of output from `file -bLnNp $filename`
//  - classic mime type - what is used for interchange i.e. text/plain - may be used for matching if other methods fail
// 
//  This is how mimetype resolving is supposed to work: if there is exactly one match for `file`
//  output, this is what we use. If there are multiple, the largest match is used. If there are
//  no results or several results with same size we look at extension, then at classic mime type
//  (using -i parameter to `file`).
// NOTE: not sure about this last thing, since -i parameter appears to just be alias



// queue/tree of mimetype data
static struct MimeData {
	char *id, *typestr, *program, *iconname, *extension, *file_output, *classic_mime;
	MimeData *next;
} *mime_first=0;


// This is used instead of strstrmulti to check if filename ends with any of extensions
char *test_extension(const char *file, const char *ext) {
	if (!file || !ext || (strlen(file)==0) || (strlen(ext)==0)) 
		return (char*)file; // this means that empty search returns true
	char *copy = strdup(ext);
	char *token = strtok(copy, "/");
	char *result = 0;
	do {
		int k = strlen(file)-strlen(token);
		if (strcmp(file+k,token) == 0) { return strdup(file+k); break; }
	} while ((token = strtok(NULL, "/"))); // double braces to silence compiler warnings :(
	free (copy);
	if (!result && (ext[strlen(ext)-1] == '/'))
		return (char*)file; // again
	return result;
}



// Read mime data from file

void get_mimedata() {
	// TODO: currently all mimes are on the same level...
	mime_first = (MimeData*) malloc(sizeof(MimeData)+1);
	MimeData *m = mime_first;

	char line[256];
	char* mime_filename = Config::find_file("mimetypes.conf");
	FILE *f = fopen(mime_filename,"r");
	bool first=true;
	while (!feof(f) && (fgets(line,255,f))) {
		if (feof(f)) break;

		// delete comments
		if (char* p=strchr(line,'#')) { *p = '\0'; }
		// delete spaces
		wstrim(line); 
		// if there's nothing left, skip
		if (strlen(line)<1) continue; 
		char *p1,*p2;
		if (!(p1 = strchr(line,'|'))) continue; // likewise

		// Allocate next element if this is not first pass
		if (!first) {
			m->next = (MimeData*) malloc(sizeof(MimeData)+1);
			m = m->next;
		}
		first=false;
		
		// parse line
		m->id = wstrim(strndup(line,p1-line));
		if (p1 && (p2 = strchr(++p1,'|'))) m->typestr = wstrim(strndup(p1,p2-p1)); else m->typestr=0;
		if (p2 && (p1 = strchr(++p2,'|'))) m->program = wstrim(strndup(p2,p1-p2)); else m->program=0;
		if (p1 && (p2 = strchr(++p1,'|'))) m->iconname = wstrim(strndup(p1,p2-p1)); else m->iconname=0;
		if (p2 && (p1 = strchr(++p2,'|'))) m->extension = wstrim(strndup(p2,p1-p2)); else m->extension=0;
		if (p1 && (p2 = strchr(++p1,'|'))) m->file_output = wstrim(strndup(p1,p2-p1)); else m->file_output=0;
		if (p2 && (p1 = strchr(++p2,'|'))) m->classic_mime = wstrim(strndup(p2,p1-p2)); else m->classic_mime=0;
	}
	m->next = 0;
	fclose(f);
}


void free_mimedata() {
	MimeData *m, *n;
	m = mime_first;
	while ((n = m->next)) { free(m); m=n; }
	mime_first=0;
}


void print_mimedata() { // for debugging
	MimeData *m = mime_first;
	while (m != 0) {
		fprintf(stderr, "ID: '%s' Name: '%s' Prog: '%s' Icon: '%s' Ext: '%s' File: '%s' MIME: '%s'\n", m->id, m->typestr, m->program, m->iconname, m->extension, m->file_output, m->classic_mime);
		m = m->next;
	}
}


// declare given MimeData as current
void MimeType::set_found(char *id) {
	if (!id) return;

	// find id
	MimeData *m = mime_first;
	while (m && strcmp(m->id,id)!=0) m = m->next;

	// copy data to cur_*
	cur_id = strdup(id);
	if (m->typestr) cur_typestr = strdup(m->typestr);
	if (m->program && (strlen(twstrim(m->program))>0)) {
		asprintf (&cur_command, "%s \"%s\"", m->program, cur_filename);
	}
	if (m->iconname) cur_iconname = strdup(m->iconname);
	else cur_iconname = strdup(DEFAULT_ICON);
}


void MimeType::set(const char* filename, bool usefind) {
	// Drop any previous data from memory
	if (cur_id) free(cur_id);
	if (cur_typestr) free(cur_typestr);
	if (cur_command) free(cur_command);
	if (cur_iconname) free(cur_iconname);
	if (cur_filename) free(cur_filename);
	cur_id=cur_typestr=cur_command=cur_iconname=cur_filename=0;

	if (filename && filename_exist(filename)) {
		cur_filename=strdup(filename);

		// Directory
		if (filename_isdir(filename)) {
			if (access(filename, R_OK || X_OK)) {
				// Not readable
				cur_id = strdup("directory/locked");
				cur_typestr = strdup("Directory (not accessible)");
				cur_iconname = strdup(FOLDERLOCKED_ICON);
			} else {
				cur_id = strdup("directory");
				cur_typestr = strdup("Directory");
				asprintf(&cur_command, "%s \"%s\"", FILE_MANAGER, filename);
				cur_iconname = strdup(FOLDER_ICON);
			}
			return;
		}

		// File not readable
		if (access(filename, R_OK)) {
			if (errno == EACCES) {
				cur_id = strdup("locked");
				cur_typestr = strdup("Can't read file");
				cur_iconname = strdup(LOCKED_ICON);
			}
			// we don't handle other errors specially
			return;
		}

		// Backup file
		if (filename[strlen(filename)-1] == '~') {
			cur_id = strdup("backup");
			cur_typestr = strdup("Backup file");
			cur_iconname = strdup(RECYCLED_ICON);
			return;
		}

		if (!mime_first) get_mimedata();

		// Stuff we need to declare before goto for visibility reasons
		MimeData *m = mime_first;
		//char buffer[256];
		char* buffer;
		int found=0, foundext = 0;
		MimeData *file_matches[50], *ext_matches[50] = {0}; // this is for if(!ext_matches[0])

		if (!usefind) goto nofind; // using goto here makes less indentation ;)

		// execute file command through libmagic
		buffer = strdup(magic_file(magic_cookie, filename));

fprintf (stderr,"(%s) File said: %s (Error: %s)\n",filename,buffer,magic_error(magic_cookie));

		// find matches for 'file' command output
		// TODO: add wildcard matching
		while (m != 0) {
			if (m->file_output && (strstr(buffer,m->file_output)))
				file_matches[found++]=m;
			m=m->next;
		}

		if (found == 1) { // one result found
			this->set_found(file_matches[0]->id); 
			free(buffer);
			return; 
		}

		if (found > 1) { // multiple results - find best match
			// We look for longest file match
			uint max=0;
			for (int i=0; i<found; i++)
				if (strlen(file_matches[i]->file_output)>max) 
					max = strlen(file_matches[i]->file_output);
fprintf(stderr, "Max: %d\n",max);
			// If all matches are empty, this is probably bogus
			if (max == 0) goto nofind;

			// Test to see if there are multiple best choices
			int j=0;
			for (int i=0; i<found; i++)
				if (strlen(file_matches[i]->file_output) == max) {
					fprintf (stderr, "Lokalni maximum '%s'\n", file_matches[i]->id);
					file_matches[j++] = file_matches[i];
				}
			// Now **file_matches should contain only maximums
			if (j==1) { 
				this->set_found(file_matches[0]->id); 
				free(buffer);
				return; 
			}

			// Compare maximums on extension
			for (int i=0; i<j; i++)
				if (test_extension(filename,file_matches[i]->extension))
					ext_matches[foundext++] = file_matches[i];

			// No extension matches - accept first result (FIXME)
			if (foundext == 0) {
				this->set_found(file_matches[0]->id);
				free(buffer);
				return;
			}
			// From here we jump to comment " // continue extension matching"
		}

	nofind:
		free(buffer);
		if (!ext_matches[0]) {
			// Try extension matching on all mimetypes
			// This code will be executed if:
			//   a) find command is disabled
			//   b) find command returned zero matches (not likely, 
			// because some mimetypes have empty 'find' field)
			//   c) all of find results have equal length and no extensions
			// match (this is probably a misconfiguration, but its possible)
			m = mime_first;
			do {
				// take care not to match empty extension
				if (m->extension 
				&& (strlen(m->extension)>0) 
				&&
				(m->extension[strlen(m->extension)-1] != '/')
				&& (test_extension(filename,m->extension))) {
					fprintf (stderr, "Extenzija '%s'\n", m->id);
					ext_matches[foundext++]=m;
				}
			} while ((m=m->next));
		}

fprintf(stderr, "Foundext: %d\n", foundext);
		// continue extension matching
		if (foundext == 1) { // one result found
			this->set_found(ext_matches[0]->id); 
			return; 
		}

		if (foundext > 1) { // multiple results - find best match
			// Code is almost the same as above
			// We look for longest extension match
			uint max=0;
			for (int i=0; i<foundext; i++)
				if (strlen(ext_matches[i]->extension)>max &&
				(ext_matches[i]->extension[strlen(ext_matches[i]->extension)-1] != '/')) 
					max = strlen(ext_matches[i]->extension);

			// Test to see if there are multiple best choices
			int j=0;
			for (int i=0; i<foundext; i++)
				if (strlen(ext_matches[i]->extension) == max)
					ext_matches[j++] = ext_matches[i];
			// Now **ext_matches should contain only maximums
			if (j==1) { this->set_found(ext_matches[0]->id); return; }

			// Now what??? we return first one whether it be the only or not!
			// FIXME
			this->set_found(file_matches[0]->id); 
			return; 
		}

		// No extension results found - this is unknown file type
		cur_id = strdup("unknown");
		cur_typestr = strdup("Unknown");
		cur_iconname = strdup(DEFAULT_ICON);
	}

}


MimeType::MimeType(const char* filename, bool usefind) {
	cur_id=cur_typestr=cur_command=cur_iconname=cur_filename=0;

	// Load mime settings file
	if (!mime_first) get_mimedata();

	// Have libmagic read its configuration data
	magic_cookie = magic_open(MAGIC_NONE);
	magic_load(magic_cookie, NULL);

	if (filename) set(filename,usefind);
}


MimeType::~MimeType() {
	if (cur_id) free(cur_id);
	if (cur_typestr) free(cur_typestr);
	if (cur_command) free(cur_command);
	if (cur_iconname) free(cur_iconname);
	if (cur_filename) free(cur_filename);

	// Free memory used by mimetype configuration
	free_mimedata(); //- should we? mimedata is static for a reason...

	// Free memory used by libmagic
	magic_close(magic_cookie);

}
