/* eLectrix - a pdf viewer
 * Copyright (C) 2010, 2011 Martin Linder <mali2297@users.sf.net>
 * 
 * This program 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.
 * 
 * This program 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/gpl-2.0.html>.
 */
#include <glib/gstdio.h>
#include "e6x-common.h"
#include "e6x-document.h"

enum
{
  CHANGED,
  FIT_TOGGLED,
  LAST_SIGNAL
};

enum
{
  RELOAD,
  PAGE_NO,
  SCALE,
  ANGLE,
  COLOR,
  MULTIPLE,
  LAST_DETAIL
};

#define E6X_DOCUMENT_GET_PRIVATE(o) \
   (G_TYPE_INSTANCE_GET_PRIVATE ((o), E6X_TYPE_DOCUMENT, E6xDocumentPrivate))

struct _E6xDocumentPrivate
{
  time_t atime;
};

static guint e6x_document_signals[LAST_SIGNAL] = { 0 };
static guint e6x_document_details[LAST_DETAIL] = { 0 };

G_DEFINE_TYPE (E6xDocument, e6x_document, G_TYPE_OBJECT)
static void e6x_document_finalize (GObject *object);
static void e6x_document_dispose (GObject *object);
static gdouble e6x_document_get_scale_from_fit (E6xDocument *doc);
static gboolean e6x_document_is_modified (E6xDocument *doc);


gboolean 
e6x_document_reload (E6xDocument *doc,
                     GError **error)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  gboolean is_success = FALSE;
  
  if (G_LIKELY (klass->reload != NULL))
    is_success = klass->reload (doc, error);
  
  if (is_success)
  {
    doc->scale = e6x_document_get_scale_from_fit (doc);
    g_signal_emit (doc,
                   e6x_document_signals[CHANGED],
                   e6x_document_details[RELOAD]);
  }
  
  return is_success;
}


gboolean 
e6x_document_save_copy (E6xDocument *doc,
                        const gchar *filename,
                        GError **error)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  gboolean is_success = FALSE;
  
  if (G_LIKELY (klass->save_copy != NULL))
    is_success = klass->save_copy (doc, filename, error);
  
  return is_success;
}


void 
e6x_document_set_page_no (E6xDocument *doc, 
                          guint page_no)
{
  gdouble new_scale = doc->scale;
  
  g_return_if_fail (doc != NULL && 
                    page_no > 0 && 
                    page_no <= doc->n_pages);  
  if (G_UNLIKELY (doc->page_no == page_no))
    return;
  
  doc->page_no = page_no;
  new_scale = e6x_document_get_scale_from_fit (doc);
  if (doc->scale != new_scale)
  {
    doc->scale = new_scale;
    g_signal_emit (doc, 
                   e6x_document_signals[CHANGED], 
                   e6x_document_details[MULTIPLE]);
  }
  else
  {
    g_signal_emit (doc, 
                   e6x_document_signals[CHANGED], 
                   e6x_document_details[PAGE_NO]);
  }
}


void 
e6x_document_set_scale (E6xDocument *doc, 
                        gdouble scale)
{
  g_return_if_fail (doc != NULL && scale > 0);
  
  if (G_UNLIKELY (doc->scale == scale))
    return;

  doc->scale = scale;
  if (doc->fit_width > 0 || doc->fit_height > 0)
    e6x_document_set_fit (doc, 0.0, 0.0);
  g_signal_emit (doc, 
                 e6x_document_signals[CHANGED], 
                 e6x_document_details[SCALE]);
}


void 
e6x_document_set_angle (E6xDocument *doc, 
                        gint angle)
{
  gdouble new_scale = doc->scale;
  
  g_return_if_fail (MODULO (angle, 90) == 0);
  if (G_UNLIKELY (doc->angle == angle))
    return;
  
  doc->angle = MODULO (angle, 360);
  new_scale = e6x_document_get_scale_from_fit (doc);
  if (doc->scale != new_scale)
  {
    doc->scale = new_scale;
    g_signal_emit (doc, 
                   e6x_document_signals[CHANGED], 
                   e6x_document_details[MULTIPLE]);
  }
  else
  {
    g_signal_emit (doc, 
                   e6x_document_signals[CHANGED], 
                   e6x_document_details[ANGLE]);
  }
}


void 
e6x_document_set_fit (E6xDocument *doc, 
                      gdouble width,
                      gdouble height)
{
  gdouble new_scale = doc->scale;
  width = MAX (0.0, width);
  height = MAX (0.0, height);
  gboolean has_toggled = (((width > 0) != (doc->fit_width > 0))
                          || ((height > 0) != (doc->fit_height > 0)));
    
  doc->fit_width = width;
  doc->fit_height = height;
  if (has_toggled)
    g_signal_emit (doc, e6x_document_signals[FIT_TOGGLED], 0);
    
  new_scale = e6x_document_get_scale_from_fit (doc);
  if (doc->scale != new_scale)
  {
    doc->scale = new_scale;
    g_signal_emit (doc, 
                   e6x_document_signals[CHANGED], 
                   e6x_document_details[SCALE]);
  }
}


void 
e6x_document_set_color_inversion (E6xDocument *doc, 
                                  gboolean inversion)
{
  if (G_UNLIKELY (doc->color_inversion == inversion))
    return;

  doc->color_inversion = inversion;
  g_signal_emit (doc, 
                 e6x_document_signals[CHANGED], 
                 e6x_document_details[COLOR]);
}


void 
e6x_document_set_show_toc (E6xDocument *doc, 
                           gboolean show)
{
  doc->show_toc = show;
}


void
e6x_document_get_page_size (E6xDocument *doc,
                            gdouble *width,
                            gdouble *height)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  
  g_return_if_fail (klass->get_page_size != NULL);
  klass->get_page_size (doc, width, height);
}


cairo_surface_t *
e6x_document_render_page (E6xDocument *doc)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  cairo_surface_t *surface = NULL;
  
  g_return_val_if_fail (klass->render_page != NULL, NULL);
  
  if (e6x_document_is_modified (doc))
    e6x_document_reload (doc, NULL);
  
  surface = klass->render_page (doc);
  
  if (doc->color_inversion)
  {
    cairo_t *cr = NULL;
    
    cr = cairo_create (surface);
    cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
    cairo_set_operator (cr, CAIRO_OPERATOR_DIFFERENCE);
    cairo_paint (cr);
    
    cairo_destroy (cr);
  }
  
  return surface;
}


gchar *
e6x_document_get_text (E6xDocument *doc, 
                       GdkRectangle rect)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  
  if (klass->get_text != NULL)
    return klass->get_text (doc, rect);
  else
    return NULL;
}


guint 
e6x_document_get_n_matches (E6xDocument *doc, 
                            guint page_no,
                            const gchar *string)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  
  g_return_val_if_fail (klass->get_n_matches != NULL, 0);
  
  return klass->get_n_matches (doc, page_no, string);
}


GdkRectangle *
e6x_document_get_nth_match (E6xDocument *doc,
                            guint page_no,
                            const gchar *string,
                            guint match_no)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  
  g_return_val_if_fail (klass->get_nth_match != NULL, NULL);
  
  return klass->get_nth_match (doc, page_no, string, match_no);
}


gboolean 
e6x_document_is_at_link (E6xDocument *doc, 
                         GdkPoint pos)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  
  if (klass->is_at_link != NULL)
    return klass->is_at_link (doc, pos);
  else
    return FALSE;
}


gboolean 
e6x_document_go_to_link (E6xDocument *doc, 
                         GdkPoint pos)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  
  if (klass->go_to_link != NULL)
    return klass->go_to_link (doc, pos);
  else
    return FALSE;
}


gchar *
e6x_document_get_tip (E6xDocument *doc, 
                      GdkPoint pos)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  
  if (klass->get_tip != NULL)
    return klass->get_tip (doc, pos);
  else
    return NULL;
}


gboolean 
e6x_document_go_to_bookmark (E6xDocument *doc, 
                             GtkTreeIter *iter)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  
  if (klass->go_to_bookmark != NULL)
    return klass->go_to_bookmark (doc, iter);
  else
    return FALSE;
}


gboolean 
e6x_document_save_att (E6xDocument *doc, 
                       GtkTreeIter *iter,
                       const gchar *filename,
                       GError **error)
{
  E6xDocumentClass *klass = E6X_DOCUMENT_GET_CLASS (doc);
  
  if (klass->save_att != NULL)
    return klass->save_att (doc, iter, filename, error);
  else
    return FALSE;
}


static void 
e6x_document_class_init (E6xDocumentClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  E6xDocumentClass *doc_class = E6X_DOCUMENT_CLASS (klass);
  
  g_type_class_add_private (klass, sizeof (E6xDocumentPrivate));

  object_class->dispose = e6x_document_dispose;
  object_class->finalize = e6x_document_finalize;

  doc_class->reload = NULL;
  doc_class->save_copy = NULL;
  doc_class->get_page_size = NULL;
  doc_class->render_page = NULL;
  doc_class->get_text = NULL;
  doc_class->get_n_matches = NULL;
  doc_class->get_nth_match = NULL;
  doc_class->is_at_link = NULL;
  doc_class->go_to_link = NULL;
  doc_class->get_tip = NULL;
  doc_class->go_to_bookmark = NULL;
  doc_class->save_att = NULL;
  
  e6x_document_signals[CHANGED] =
    g_signal_new ("changed",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | 
                  G_SIGNAL_NO_HOOKS | G_SIGNAL_DETAILED,
                  0, NULL, NULL,
                  g_cclosure_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);
  e6x_document_signals[FIT_TOGGLED] =
    g_signal_new ("fit-toggled",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | 
                  G_SIGNAL_NO_HOOKS,
                  0, NULL, NULL,
                  g_cclosure_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);
   
  e6x_document_details[RELOAD] = g_quark_from_string ("reload");
  e6x_document_details[PAGE_NO] = g_quark_from_string ("page-no");
  e6x_document_details[SCALE] = g_quark_from_string ("scale");
  e6x_document_details[ANGLE] = g_quark_from_string ("angle");
  e6x_document_details[COLOR] = g_quark_from_string ("color");
  e6x_document_details[MULTIPLE] = g_quark_from_string ("multiple");
}


static void 
e6x_document_init (E6xDocument *doc)
{
  E6xDocumentPrivate *priv = E6X_DOCUMENT_GET_PRIVATE (doc);
  
  priv->atime = time (NULL);
  
  doc->priv = priv;
  doc->filename = NULL;
  doc->passwd = NULL;
  doc->title = NULL;
  doc->info = NULL;
  doc->toc = NULL;
  doc->attlist = NULL;
  doc->page_no = 1;
  doc->n_pages = 1;
  doc->scale = 1.0;
  doc->angle = 0;
  doc->fit_width = 0.0;
  doc->fit_height = 0.0;
  doc->color_inversion = FALSE;
  doc->show_toc = FALSE;
}


static void 
e6x_document_finalize (GObject *object)
{
}


static void 
e6x_document_dispose (GObject *object)
{
  E6xDocument *doc = E6X_DOCUMENT (object);

  if (doc->filename != NULL)
  {
    g_free (doc->filename);
    doc->filename = NULL;
  }
  if (doc->passwd != NULL)
  {
    g_free (doc->passwd);
    doc->passwd = NULL;
  }
  if (doc->title != NULL)
  {
    g_free (doc->title);
    doc->title = NULL;
  }
  if (doc->info != NULL)
  {
    g_strfreev (doc->info);
    doc->info = NULL;
  }
}


static gdouble 
e6x_document_get_scale_from_fit (E6xDocument *doc)
{
  gdouble page_width = 0.0, page_height = 0.0;
  gdouble width_ratio = 0.0, height_ratio = 0.0;

  if (MAX (doc->fit_width, doc->fit_height) <= 0.0)
    return doc->scale;
  
  e6x_document_get_page_size (doc, &page_width, &page_height);
  if (MIN (page_width, page_height) <= 0.0)
    return doc->scale;
  
  switch (doc->angle)
  {
  case 0:
    /* Fall through */
  case 180:
    width_ratio = (doc->fit_width / page_width);
    height_ratio = (doc->fit_height / page_height);
    break;
  case 90:
    /* Fall through */
  case 270:
    width_ratio = (doc->fit_width / page_height);
    height_ratio = (doc->fit_height / page_width);
    break;
  default:
    g_warn_if_reached ();
    break;
  }
  
  if ((width_ratio > 0) && (height_ratio > 0))
    return MIN (width_ratio, height_ratio);
  else if (width_ratio > 0)
    return width_ratio;
  else if (height_ratio > 0)
    return height_ratio;
  else
    return doc->scale;
}


static gboolean
e6x_document_is_modified (E6xDocument *doc)
{
  struct stat buf;
  
  g_stat (doc->filename, &buf);
  
  if (difftime (buf.st_mtime, doc->priv->atime) > 0)
  {
    doc->priv->atime = buf.st_mtime;
    return TRUE;
  }
  
  return FALSE;
}
