/*
 * File Name: images_scanner.cpp
 */

/*
 * This file is part of uds-plugin-comics.
 *
 * uds-plugin-images is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * uds-plugin-images is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Copyright (C) 2008 iRex Technologies B.V.
 * All rights reserved.
 */

#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifndef WIN32
#include <unistd.h>
#endif

#include <cassert>
#include <iostream>
#include <algorithm>
#include <glib.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gdk-pixbuf/gdk-pixbuf-core.h>
#include <gio/gio.h>
#ifndef WIN32
#include <libexif/exif-loader.h>
#endif

#include "json/json.h"
#include "utils.h"
#include "images_scanner.h"
#include "image_page.h"
#include "log.h"

#define IGNORE_ROTATION 1
//#define IGNORE_REALSIZE 1

namespace comics
{

/*
 * class ImagesScanner
 */
ImagesScanner::ImagesScanner(void)
:filepath("")
,lastimage(NULL)
,lastimagepath("")
,active(-1) 
{
    LOGPRINTF("entry");

	cbzfile = cbzscanner = NULL;

	mutex = g_mutex_new();
	scannermutex = g_mutex_new();
    get_supported_extnames();
}

ImagesScanner::~ImagesScanner(void)
{
	if(cbzfile) {
		unzClose(cbzfile);
	}
	if(cbzscanner) {
		unzClose(cbzscanner);
	}
	g_mutex_free(mutex);
	g_mutex_free(scannermutex);
    LOGPRINTF("entry");
}

bool ImagesScanner::is_comic(const std::string & filename)
{
	return get_archive_type(filename) != ARC_UNK;
}

bool ImagesScanner::is_image(const std::string & filename)
{
    // Only check the extension name to imporve the performance of scanning. 
    bool ret = check_extname(filename);

    LOGPRINTF("%s is image? %d", filename.c_str(), ret);

    return ret;
}

int ImagesScanner::get_position(const std::string & path)
{
    ImagesIter begin = images.begin();
    ImagesIter end   = images.end();
    for(ImagesIter iter = begin; iter != end; ++iter)
    {
        if ((*iter)->path == path)
        {
            LOGPRINTF("%d", static_cast<int>((iter - begin) + 1));

            return static_cast<int>((iter - begin) + 1);
        }
    }
    return -1;
}

static void calc_size_prepared(GdkPixbufLoader *loader, 
		 int              width,
		 int              height,
		 gpointer         data)
{
	Image *image = (Image *)data;

	g_return_if_fail (width > 0 && height > 0);

	image->is_calculated = true;
	image->width = width;
	image->height = height;

	gdk_pixbuf_loader_set_size (loader, 0, 0);
}

struct SizePreparedArgs
{
	recalc_size_callback callback;
	Image *image;
	void *user;
};

static void get_pixbuf_size_prepared(GdkPixbufLoader *loader, 
	int              width,
	int              height,
	gpointer         data)
{
	SizePreparedArgs *args = (SizePreparedArgs *)data;

	g_return_if_fail (width > 0 && height > 0);

	args->image->is_calculated = true;
	args->image->width = width;
	args->image->height = height;

	if(args->callback != NULL)
	{
		bool should_resize = args->callback(&width, &height, args->user);
		
		if(should_resize)
		{
			gdk_pixbuf_loader_set_size(loader, width, height);
		}
	}
}

Image *ImagesScanner::update_calc_zip(const std::string & filename)
{
	int position = get_position(filename);

	// Check to see if we need to update
	if(position == -1) return NULL;
	position--; // Prevent off-by-one errors
	if(images[position]->is_calculated) return images[position];
	Image *image = images[position];

	g_mutex_lock(scannermutex);
	if (unzLocateFile(cbzscanner, filename.c_str(), 0) == UNZ_OK)
	{
		if (unzOpenCurrentFile(cbzscanner) == UNZ_OK) 
		{
			unsigned char buffer[2048];
			unsigned int bytes_read;

			GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
			g_signal_connect(loader, "size-prepared", G_CALLBACK(calc_size_prepared), image);
			
			//ExifLoader *exif_loader = exif_loader_new();
			//bool exif_found = false;

			while (!unzeof(cbzscanner)) {
				bytes_read = unzReadCurrentFile(cbzscanner, buffer, sizeof(buffer));
				if (bytes_read > 0) {
					/*if (!exif_found && !exif_loader_write(exif_loader, buffer, bytes_read))
					{						
						ExifData *exif_data = exif_loader_get_data(exif_loader);
						
						if(exif_data != NULL)
						{
							update_rotation(image, exif_data);
							exif_data_unref(exif_data);
						} else {
							image->rotation = Clockwise_Degrees_0;
						}
						
						exif_found = true;
					}*/
					
					if (!gdk_pixbuf_loader_write (loader, buffer, bytes_read, NULL))
						break;
				}
				if (image->is_calculated)
					break;
			}			

			unzCloseCurrentFile(cbzscanner);
			gdk_pixbuf_loader_close(loader, NULL);
			g_object_unref(loader);
			//exif_loader_unref(exif_loader);
		} 
	}
	g_mutex_unlock(scannermutex);

	return image;
}

GdkPixbuf *ImagesScanner::get_pixbuf_zip(const std::string & filename, recalc_size_callback size_callback, void *user_data)
{
	int position = get_position(filename);

	// Check to see if we need to update
	if(position == -1) return NULL;
	position--; // Prevent off-by-one errors
	Image *image = images[position];
	GdkPixbuf *result = NULL;

	g_mutex_lock(mutex);
	if (unzLocateFile(cbzfile, filename.c_str(), 0) == UNZ_OK)
	{
		if (unzOpenCurrentFile(cbzfile) == UNZ_OK) 
		{
			unsigned char buffer[4096 * 4];
			unsigned int bytes_read;
			SizePreparedArgs args;
			
			args.callback = size_callback;
			args.image = image;
			args.user = user_data;

			GdkPixbufLoader *loader = gdk_pixbuf_loader_new ();
			g_signal_connect(loader, "size-prepared", G_CALLBACK(get_pixbuf_size_prepared), &args);

			while (!unzeof(cbzfile)) {
				bytes_read = unzReadCurrentFile(cbzfile, buffer, sizeof(buffer));
				if (bytes_read > 0) {
					if (!gdk_pixbuf_loader_write (loader, buffer, bytes_read, NULL))
					{
						WARNPRINTF("Whoops.");
						break;
					}
				}
			}			

			unzCloseCurrentFile(cbzfile);			
			if(gdk_pixbuf_loader_close(loader, NULL))
			{
				result = gdk_pixbuf_loader_get_pixbuf(loader);
				g_object_ref(result);
			}			
			g_object_unref(loader);
			
			if(result == NULL) WARNPRINTF("No valid pixbuf loaded");
		} 
	}
	g_mutex_unlock(mutex);

	return result;
}

GdkPixbuf *ImagesScanner::get_pixbuf(const std::string & filename, recalc_size_callback size_callback, void *user_data)
{
	switch(arctype)
	{
		case ARC_ZIP:
			return get_pixbuf_zip(filename, size_callback, user_data);
			break;
		case ARC_RAR:
			//return get_pixbuf_rar(filename, size_callback, user_data);
			break;
		default:
			break;
	}
	
	return NULL;
}

Image *ImagesScanner::update_calc(const std::string & filename)
{
	switch(arctype)
	{
		case ARC_ZIP:
			return update_calc_zip(filename);
			break;
		case ARC_RAR:
			//return update_calc_rar(filename);
			break;
		default:
			break;
	}
	
	return NULL;
}

#ifndef WIN32
void ImagesScanner::update_rotation(Image * image, ExifData *ped)
{	
    ExifEntry * pee = exif_data_get_entry(ped, EXIF_TAG_ORIENTATION);
    if (pee == 0) 
    {
        exif_data_unref(ped);
        return; 
    }

    // CAUTION: The character set of the returned string is not defined.
    // It may be UTF-8, latin1, the native encoding of the
    // computer, or the native encoding of the camera.
    const unsigned int maxlen = 1024;
    char val[maxlen] = {0};

    if (exif_entry_get_value (pee, val, maxlen))
    {
        // Conversion table for EXIT orientation to rotation degrees.
        // See also:
        //   http://www.impulseadventure.com/photo/exif-orientation.html
        //   http://sylvana.net/jpegcrop/exif_orientation.html
        const struct
              {
                  const char                    *exif_orientation;
                  const PluginRotationDegree    rotation;
              } exif_conversion_tbl[]
              =
              {
                  { "top - left",     Clockwise_Degrees_0   },
                  { "top - right",    Clockwise_Degrees_0   },
                  { "bottom - right", Clockwise_Degrees_180 },
                  { "bottom - left",  Clockwise_Degrees_180 },
                  { "left - top",     Clockwise_Degrees_270 },
                  { "right - top",    Clockwise_Degrees_270 },
                  { "right - bottom", Clockwise_Degrees_90  },
                  { "left - bottom",  Clockwise_Degrees_90  },
                  { NULL,             Clockwise_Degrees_0   }
              };

        for (int i = 0; exif_conversion_tbl[i].exif_orientation; i++)
        {
            if ( strcmp(val, exif_conversion_tbl[i].exif_orientation) == 0 )
            {
                image->rotation = exif_conversion_tbl[i].rotation;
                break;
            }
        }
    }
}
#endif

int ImagesScanner::scan_archive(const std::string & filename,
                                SortType sort_type,
                                bool sort_ascending,
								std::map<std::string, std::string> & metadata)
{
	filepath = filename;
	
	arctype = get_archive_type(filename);
	if (g_file_test(filename.c_str(), (GFileTest)G_FILE_TEST_IS_REGULAR))
	{
		switch(arctype)
		{
			case ARC_ZIP:
				return scan_zip(filename, sort_type, sort_ascending, images, metadata);
				break;
			case ARC_RAR:
				//return scan_rar(filename, sort_type, sort_ascending, images);
				break;
			default:
				break;
		}
	}
	
	return 0;
}

int ImagesScanner::scan_zip(const std::string & filename,
                                SortType sort_type,
                                bool sort_ascending,
                                Images & results,
								std::map<std::string, std::string> & metadata)
{
	cbzfile = unzOpen(filename.c_str());
	cbzscanner = unzOpen(filename.c_str());
		
	if (cbzfile != NULL)
	{
		int status = unzGoToFirstFile(cbzfile);
		while( status == UNZ_OK ) {
			char nameBuffer[1024];
			ImagePtr image;

			// get filename
			unzGetCurrentFileInfo(cbzfile, NULL, nameBuffer, sizeof(nameBuffer), NULL, 0, NULL, 0);
			if (is_image(nameBuffer) && (image = new Image)) 
			{
				image->path = std::string(nameBuffer);
				image->is_calculated = false;
				image->width = -1;
				image->height = -1;
				image->is_rotation_calculated = false;
				image->rotation = Clockwise_Degrees_0;
				results.push_back(image);
			}
			
			status = unzGoToNextFile(cbzfile);
		}
		
		if(results.size() > 0)
		{
			// Check for JSON in the 
			unz_global_info file_info;
			unzGetGlobalInfo(cbzfile, &file_info);
			if(file_info.size_comment > 0)
			{
				Json::Reader reader;
				char *buffer = new char[file_info.size_comment + 1];
				std::string json_text;
				
				unzGetGlobalComment(cbzfile, buffer, file_info.size_comment + 1);
				json_text = buffer;
				delete [] buffer;
				
				parse_json(json_text, metadata);
			}
			
			sort_images(results, sort_type, sort_ascending);
		}
	}
	
	return (results.size() > 0 ? 1 : 0);
}

void ImagesScanner::parse_json(std::string & text, std::map<std::string, std::string> & metadata_map)
{
	Json::Reader reader;
	Json::Value root;
	
	// Parse and check to see if we have some real JSON.
	if(!reader.parse(text, root, false)) return;
	
	// Make sure we have an actual ComicBookInfo format.
	Json::Value metadata = root["ComicBookInfo/1.0"];
	if(metadata.isNull()) return;
	
	if(!metadata["publisher"].isNull()) {
	    metadata_map.insert(std::make_pair("publisher", metadata["publisher"].asString()));
	}
	
	if(!metadata["title"].isNull()) {
		metadata_map.insert(std::make_pair("title", metadata["title"].asString()));
	}
	
	if(metadata["credits"].isArray()) {
		Json::Value &credits = metadata["credits"];
		std::string writer = "";
		std::string artist = "";
		
		int size = credits.size();
		for(int i = 0; i < size; i++) 
		{
			if(credits[i].get("primary", false).asBool())
			{
				std::string role = credits[i].get("role", "No Role").asString();
				
				if(role == "Writer") writer = credits[i].get("person", "").asString();
				if(role == "Artist") artist = credits[i].get("person", "").asString();
			}
		}
		
		if(writer != "" && artist != "" && strcasecmp(writer.c_str(), artist.c_str()) != 0)
		{
			metadata_map.insert(std::make_pair("author", writer + " & " + artist));			
		}
		else if(writer != "")
		{
			metadata_map.insert(std::make_pair("author", writer));			
		}
		else if(artist != "")
		{
			metadata_map.insert(std::make_pair("author", artist));						
		}
	}
}

void ImagesScanner::get_supported_extnames(void)
{
    // Get the formats supported by gdk_pixbuf.
    GSList * list = gdk_pixbuf_get_formats();
    for (GSList * ptr = list; ptr; ptr = ptr->next)
    {
        GdkPixbufFormat * format = (GdkPixbufFormat*)ptr->data;
        gchar ** extensions = gdk_pixbuf_format_get_extensions(format);
        if (extensions)
        {
            for (int i = 0; extensions[i] != 0; i++)
            {
                LOGPRINTF("%s", extensions[i]);
                extnames.push_back(extensions[i]);
            }
            g_strfreev(extensions);
        }
    }
    
    if (list)
    {
        g_slist_free(list);
    }
}

ArchiveType ImagesScanner::get_archive_type(const std::string & filename)
{
	const char *extensions[] = { "cbz", NULL };
	const ArchiveType types[] = { ARC_ZIP , ARC_UNK };

    char ext[MAX_PATH_LEN];
    if (get_ext_name(filename.c_str(), ext, MAX_PATH_LEN))
    {
		unsigned int i = 0;
		while(extensions[i] != NULL)
		{
			if (!g_ascii_strncasecmp(extensions[i], ext, MAX_PATH_LEN))
			{
				return types[i];
			}
		}
    }

	return ARC_UNK;
}

bool ImagesScanner::check_extname(const std::string & filename)
{
    bool ret = false;

    char ext[MAX_PATH_LEN];
    if (get_ext_name(filename.c_str(), ext, MAX_PATH_LEN))
    {
        for (unsigned int i = 0; i < extnames.size(); i++)
        {
            if (!g_ascii_strncasecmp(extnames[i].c_str(), 
            ext, MAX_PATH_LEN))
            {
                ret = true;
            }
        }
    }

    LOGPRINTF("%s is image? %d", filename.c_str(), ret);

    return ret;
}

void ImagesScanner::sort_images(Images & results,
                                SortType sort_type, 
                                bool sort_ascending)
{
    ImagesIter begin = results.begin();
    ImagesIter end = results.end();
    ImagesIter iter;
#ifndef WIN32  
    // Calculate the sort field before sorting.
    // If sort by filepath, no need to calculate.
    if (sort_type != BY_FILEPATH)
    {
        char tmp[MAX_PATH_LEN] = {0};
        struct stat stat_buf;
        
        for (iter = begin; iter != end; ++iter)
        {
            tmp[0] = '\0';
            switch (sort_type)
            {
                case BY_FILENAME:
                    get_file_name((*iter)->path.c_str(), tmp, MAX_PATH_LEN);
                    break;
                case BY_EXT:
                    get_ext_name((*iter)->path.c_str(), tmp, MAX_PATH_LEN);
                    break;
                case BY_DATE:
                    if ((stat((*iter)->path.c_str(), &stat_buf) == 0)
                     && (S_ISREG(stat_buf.st_mode) != 0))
                    {
                        // What is date?
                        struct tm local_time;
                        localtime_r(&stat_buf.st_mtime, &local_time);
                        strftime(tmp, MAX_PATH_LEN, 
                                "%Y-%m-%dT%H:%M:%S", &local_time);
                    }
                    break;
                case BY_SIZE:
                    if ((stat((*iter)->path.c_str(), &stat_buf) == 0)
                        && (S_ISREG(stat_buf.st_mode)))
                    {
                        snprintf(tmp, MAX_PATH_LEN, 
                                "%ld", stat_buf.st_blocks * 512);
                    }    
                    break;
                default:
                    break;
            }

            (*iter)->sort_field = tmp;
        }
    }

    // Sorting
    switch (sort_type)
    {
        case BY_FILENAME:
            if (sort_ascending) 
            { 
                std::sort(begin, end, less_by_filename); 
            }
            else 
            { 
                std::sort(begin, end, greater_by_filename); 
            }
            break;

        case BY_EXT:
            if (sort_ascending) 
            { 
                std::sort(begin, end, less_by_ext); 
            }
            else 
            { 
                std::sort(begin, end, greater_by_ext); 
            }
            break;

        case BY_DATE:
            if (sort_ascending) 
            { 
                std::sort(begin, end, less_by_date); 
            }
            else 
            { 
                std::sort(begin, end, greater_by_date); 
            }
            break;

        case BY_SIZE:
            if (sort_ascending) 
            { 
                std::sort(begin, end, less_by_size); 
            }
            else 
            { 
                std::sort(begin, end, greater_by_size); 
            }
            break;

        case BY_FILEPATH:
        default:
            if (sort_ascending) 
            { 
                std::sort(begin, end, less_by_filepath); 
            }
            else 
            { 
                std::sort(begin, end, greater_by_filepath); 
            } 
            break;
    }

    // Clear the sort field after sorting.
    if (sort_type != BY_FILEPATH)
    {
        for (iter = begin; iter != end; ++iter)
        {
            (*iter)->sort_field.clear();
        }
    }
#endif
    // Calculate the 'active'.
    int i = 0;
    for (iter = begin; iter != end; ++iter)
    {
        if ((*iter)->path == filepath)
        {
            active = i;
            break;
        }
        i++;
    }
}

#ifndef WIN32
bool ImagesScanner::greater_by_filepath(Image * a, Image * b)
{
    int ret = 0;

    if ((!a->path.empty()) && (!b->path.empty()))
    {
        ret = strcasecmp(a->path.c_str(), b->path.c_str());
    }
    else if ((!a->path.empty()) && (b->path.empty()))
    {
        ret = 1;
    }
    else if ((a->path.empty()) && (!b->path.empty()))
    {
        ret = -1;
    }
    
    return ((ret > 0) ? true : false);
}

bool ImagesScanner::greater_by_filename(Image * a, Image * b)
{
    int ret = 0;
    
    if ((!a->sort_field.empty()) && (!b->sort_field.empty()))
    {
        ret = strcasecmp(a->sort_field.c_str(), b->sort_field.c_str());
    }
    else if ((!a->sort_field.empty()) && (b->sort_field.empty()))
    {
        ret = 1;
    }
    else if ((a->sort_field.empty()) && (!b->sort_field.empty()))
    {
        ret = -1;
    }

    if (!ret) { ret = strcasecmp(a->path.c_str(), b->path.c_str());}
    return ((ret > 0) ? true : false);
}

bool ImagesScanner::greater_by_ext(Image * a, Image * b)
{
    int ret = 0;

    if ((!a->sort_field.empty()) && (!b->sort_field.empty()))
    {
        ret = strcasecmp(a->sort_field.c_str(), b->sort_field.c_str());
    }
    else if ((!a->sort_field.empty()) && (b->sort_field.empty()))
    {
        ret = 1;
    }
    else if ((a->sort_field.empty()) && (!b->sort_field.empty()))
    {
        ret = -1;
    }

    if (!ret) { ret = strcasecmp(a->path.c_str(), b->path.c_str());}
    return ((ret > 0) ? true : false);
}

bool ImagesScanner::greater_by_date(Image * a, Image * b)
{
    int ret = 0;

    if ((!a->sort_field.empty()) && (!b->sort_field.empty()))
    {
        struct tm tm_a, tm_b;
        strptime(a->sort_field.c_str(), "%Y-%m-%dT%H:%M:%S", &tm_a);
        strptime(b->sort_field.c_str(), "%Y-%m-%dT%H:%M:%S", &tm_b);

        time_t time_a = mktime(&tm_a); 
        time_t time_b = mktime(&tm_b);

        double diff = difftime(time_a, time_b);
        ret = (int)diff;
    }
    else if ((!a->sort_field.empty()) && (b->sort_field.empty()))
    {
        ret = 1;
    }
    else if ((a->sort_field.empty()) && (!b->sort_field.empty()))
    {
        ret = -1;
    }
        
    if (!ret) { ret = strcasecmp(a->path.c_str(), b->path.c_str());}
    return ((ret > 0) ? true : false);
}

bool ImagesScanner::greater_by_size(Image * a, Image * b)
{
    int ret = 0;

    if ((!a->sort_field.empty()) && (!b->sort_field.empty()))
    {
        ret = atoi(a->sort_field.c_str()) - atoi(b->sort_field.c_str());
    }
    else if ((!a->sort_field.empty()) && (b->sort_field.empty()))
    {
        ret = 1;
    }
    else if ((a->sort_field.empty()) && (!b->sort_field.empty()))
    {
        ret = -1;
    }
 
    if (!ret) { ret = strcasecmp(a->path.c_str(), b->path.c_str());}
    return ((ret > 0) ? true : false);
}

bool ImagesScanner::less_by_filepath(Image * a, Image * b)
{
    return greater_by_filepath(b, a);
}

bool ImagesScanner::less_by_filename(Image * a, Image * b)
{
    return greater_by_filename(b, a);
}

bool ImagesScanner::less_by_ext(Image * a, Image * b)
{
    return greater_by_ext(b, a);
}

bool ImagesScanner::less_by_date(Image * a, Image * b)
{
    return greater_by_date(b, a);
}

bool ImagesScanner::less_by_size(Image * a, Image * b)
{
    return greater_by_size(b, a);
}
#endif

}; // namespace image


