//
// Copyright (C) 2011-2012 Robert Dyer, Rico Tzschichholz
//
// This file is part of Plank.
//
// Plank 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 3 of the License, or
// (at your option) any later version.
//
// Plank 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/>.
//

namespace Plank {
  /**
   * The base class for all dock items.
   */
  public abstract class DockItem : DockElement {
    /**
     * Signal fired when the .dockitem for this item was deleted.
     */
    public signal void deleted ();

    /**
     * The dock item's icon.
     */
    public string Icon { get; set; default = ""; }

    protected Gdk.Pixbuf? ForcePixbuf { get; set; default = null; }

    /**
     * The count for the dock item.
     */
    public int64 Count { get; set; default = 0; }

    /**
     * Show the item's count or not.
     */
    public bool CountVisible { get; set; default = false; }

    /**
     * The progress for this dock item.
     */
    public double Progress { get; set; default = 0; }

    /**
     * Show the item's progress or not.
     */
    public bool ProgressVisible { get; set; default = false; }

    /**
     * Should this item allow Zooming.
     */
    public bool AllowZoom { get; set; default = true; }

    int position = -1;
    /**
     * The dock item's position on the dock.
     */
    public int Position {
      get {
        return position;
      }
      set {
        if (position == value)
          return;

        if (LastPosition != position)
          LastPosition = position;

        position = value;

        // Only trigger animation if this isn't the initial position set
        if (LastPosition > -1) {
          LastMove = GLib.get_monotonic_time ();
          State |= ItemState.MOVE;
        }
      }
    }

    /**
     * The dock item's last position on the dock.
     */
    public int LastPosition { get; protected set; default = -1; }

    /**
     * The item's current state.
     */
    public ItemState State { get; protected set; default = ItemState.NORMAL; }

    /**
     * The indicator shown for the item.
     */
    public IndicatorState Indicator { get; protected set; default = IndicatorState.NONE; }

    /**
     * The average color of this item's icon.
     */
    public Color AverageIconColor { get; protected set; default = Color () { red = 0.0, green = 0.0, blue = 0.0, alpha = 0.0 }; }

    /**
     * The filename of the preferences backing file.
     */
    public string DockItemFilename {
      owned get { return Prefs.get_filename (); }
    }

    /**
     * The launcher associated with this item.
     */
    public string Launcher {
      get { return Prefs.Launcher; }
    }

    /**
     * The underlying preferences for this item.
     */
    public DockItemPreferences Prefs { get; construct; }

    SurfaceCache<DockItem> buffer;
    SurfaceCache<DockItem> background_buffer;
    Surface? foreground_surface = null;

    FileMonitor? launcher_file_monitor = null;
    FileMonitor? icon_file_monitor = null;

    bool launcher_exists = false;
    uint removal_timer_id = 0U;

    /**
     * Creates a new dock item.
     */
    protected DockItem () {
      GLib.Object (Prefs : new DockItemPreferences ());
    }

    construct
    {
      buffer = new SurfaceCache<DockItem> (SurfaceCacheFlags.NONE);
      background_buffer = new SurfaceCache<DockItem> (SurfaceCacheFlags.ALLOW_SCALE);

      Prefs.deleted.connect (handle_deleted);
      Prefs.notify["Launcher"].connect (handle_launcher_changed);

      DrawingService.get_icon_theme ().changed.connect (icon_theme_changed);
      notify["Icon"].connect (icon_changed);
      notify["ForcePixbuf"].connect (icon_changed);

      notify["Count"].connect (reset_foreground_buffer);
      notify["CountVisible"].connect (reset_foreground_buffer);
      notify["Progress"].connect (reset_foreground_buffer);
      notify["ProgressVisible"].connect (reset_foreground_buffer);

      launcher_file_monitor_start ();
      if (ForcePixbuf == null)
        icon_file_monitor_start ();
    }

    ~DockItem () {
      buffer.clear ();
      background_buffer.clear ();

      Prefs.deleted.disconnect (handle_deleted);
      Prefs.notify["Launcher"].disconnect (handle_launcher_changed);

      DrawingService.get_icon_theme ().changed.disconnect (icon_theme_changed);
      notify["Icon"].disconnect (icon_changed);
      notify["ForcePixbuf"].disconnect (icon_changed);

      notify["Count"].disconnect (reset_foreground_buffer);
      notify["CountVisible"].disconnect (reset_foreground_buffer);
      notify["Progress"].disconnect (reset_foreground_buffer);
      notify["ProgressVisible"].disconnect (reset_foreground_buffer);

      launcher_file_monitor_stop ();
      icon_file_monitor_stop ();

      if (stop_removal ())
        @delete ();
    }

    /**
     * Signal handler called when the underlying preferences file is deleted.
     */
    void handle_deleted () {
      deleted ();
    }

    /**
     * Parses the associated launcher and e.g. sets the icon and text from it.
     */
    protected virtual void load_from_launcher () {
      // No default implementation needed
    }

    void handle_launcher_changed () {
      launcher_file_monitor_stop ();

      load_from_launcher ();

      launcher_file_monitor_start ();
    }

    /**
     * Deletes the underlying preferences file.
     */
    public void delete () {
      launcher_file_monitor_stop ();

      Prefs.delete ();
    }

    /**
     * The item is a separator.
     */
    public bool is_separator () {
      if (DockItemFilename == null) {
        return false;
      }

      try {
        var separator_regexp = new Regex ("separator(-\\d+)?\\.dockitem");
        if (separator_regexp.match (DockItemFilename)) {
          return true;
        }
      } catch (RegexError e) {
        return false;
      }

      return false;
    }

    /**
     * Resets the buffer for this item's icon and requests a redraw.
     */
    protected void reset_icon_buffer () {
      buffer.clear ();
      background_buffer.clear ();
      foreground_surface = null;

      needs_redraw ();
    }

    /**
     * Resets the buffers for this item's icon.
     */
    public override void reset_buffers () {
      background_buffer.clear ();
      foreground_surface = null;
    }

    public void unset_move_state () {
      State &= ~ItemState.MOVE;
    }

    void reset_foreground_buffer () {
      foreground_surface = null;

      needs_redraw ();
    }

    void icon_theme_changed () {
      // Put Gtk.IconTheme.changed emmitted signals in idle queue to avoid
      // race conditions with concurrent handles
      Gdk.threads_add_idle_full (GLib.Priority.LOW, () => {
        reset_icon_buffer ();
        return false;
      });
    }

    void icon_changed () {
      icon_file_monitor_stop ();

      if (ForcePixbuf == null)
        icon_file_monitor_start ();

      reset_icon_buffer ();
    }

    [CCode (instance_pos = -1)]
    void icon_file_changed (File f, File? other, FileMonitorEvent event) {
      switch (event) {
      case FileMonitorEvent.CHANGES_DONE_HINT :
        reset_icon_buffer ();
        break;
        default :
        break;
      }
    }

    void icon_file_monitor_start () {
      var icon_file = DrawingService.try_get_icon_file (Icon);
      if (icon_file == null || icon_file.get_uri_scheme () != "file")
        return;

      try {
        icon_file_monitor = icon_file.monitor_file (0);
        icon_file_monitor.changed.connect (icon_file_changed);
      } catch (Error e) {
        critical ("Unable to watch the icon file '%s'", icon_file.get_path () ?? "");
        debug (e.message);
      }
    }

    void icon_file_monitor_stop () {
      if (icon_file_monitor == null)
        return;

      icon_file_monitor.changed.disconnect (icon_file_changed);
      icon_file_monitor.cancel ();
      icon_file_monitor = null;
    }

    [CCode (instance_pos = -1)]
    void launcher_file_changed (File f, File? other, FileMonitorEvent event) {
      switch (event) {
      case FileMonitorEvent.CHANGES_DONE_HINT :
        Logger.verbose ("Launcher file '%s' changed, reloading", f.get_uri ());

        load_from_launcher ();
        break;
      case FileMonitorEvent.DELETED :
      case FileMonitorEvent.MOVED_OUT :
        debug ("Launcher file '%s' deleted, item is invalid now", f.get_uri ());

        launcher_exists = false;
        LastValid = GLib.get_monotonic_time ();
        State |= ItemState.INVALID;

        schedule_removal_if_needed ();
        break;
      case FileMonitorEvent.CREATED:
      case FileMonitorEvent.MOVED_IN:
        debug ("Launcher file '%s' created, item is valid again", f.get_uri ());

        launcher_exists = true;
        State &= ~ItemState.INVALID;

        stop_removal ();
        break;
      case FileMonitorEvent.RENAMED:
        if (other == null)
          break;

        var launcher = other.get_uri ();

        // Rename of launcher file to new name
        if (launcher_exists) {
          Logger.verbose ("Launcher file '%s' moved to '%s'", f.get_uri (), launcher);

          replace_launcher (launcher);

          load_from_launcher ();
          break;
        }

        // Rename of temporary file to launcher name
        debug ("Launcher file '%s' created from rename of '%s', item is valid again", launcher, f.get_uri ());

        launcher_exists = true;
        State &= ~ItemState.INVALID;

        stop_removal ();
        break;
      default:
        break;
      }

      needs_redraw ();
    }

    void launcher_file_monitor_start () {
      if (launcher_file_monitor != null)
        return;

      unowned string? launcher = Prefs.Launcher;
      if (launcher == null || launcher == "") {
        State &= ~ItemState.INVALID;
        return;
      }

      try {
        var launcher_file = File.new_for_uri (launcher);
        launcher_exists = launcher_file.query_exists ();
        launcher_file_monitor = launcher_file.monitor_file (FileMonitorFlags.WATCH_MOVES);
        launcher_file_monitor.changed.connect (launcher_file_changed);
      } catch {
        warning ("Unable to watch the launcher file '%s'", launcher);
      }
    }

    void launcher_file_monitor_stop () {
      if (launcher_file_monitor == null)
        return;

      launcher_file_monitor.changed.disconnect (launcher_file_changed);
      launcher_file_monitor.cancel ();
      launcher_file_monitor = null;
    }

    void replace_launcher (string launcher) {
      if (launcher == Prefs.Launcher)
        return;

      launcher_file_monitor_stop ();
      Prefs.notify["Launcher"].disconnect (handle_launcher_changed);
      Prefs.Launcher = launcher;
      Prefs.notify["Launcher"].connect (handle_launcher_changed);
      launcher_file_monitor_start ();
    }

    bool schedule_removal_if_needed () {
      if (removal_timer_id > 0U)
        return true;

      if (launcher_file_monitor == null || is_valid ())
        return false;

      removal_timer_id = Gdk.threads_add_timeout (ITEM_INVALID_DURATION, () => {
        removal_timer_id = 0U;
        if (!is_valid ())
          @delete ();
        return false;
      });

      return true;
    }

    bool stop_removal () {
      if (removal_timer_id == 0U)
        return false;

      Source.remove (removal_timer_id);
      removal_timer_id = 0U;

      return true;
    }

    /**
     * Returns the surface for this item.
     *
     * It might trigger an internal redraw if the requested size
     * isn't cached yet.
     *
     * @param width width of the icon surface
     * @param height height of the icon surface
     * @param model existing surface to use as basis of new surface
     * @return the surface for this item which may not be changed
     */
    public Surface get_surface (int width, int height, Surface model) {
      return buffer.get_surface<DockItem> (width, height, model, (DrawFunc<DockItem>) internal_get_surface, null);
    }

    [CCode (instance_pos = -1)]
    Surface internal_get_surface (int width, int height, Surface model, DrawDataFunc<DockItem>? draw_data_func) {
      var surface = new Surface.with_surface (width, height, model);

      Logger.verbose ("DockItem.draw_icon (width = %i, height = %i)", width, height);
      draw_icon (surface);

      AverageIconColor = surface.average_color ();

      return surface;
    }

    /**
     * Returns the background surface for this item.
     *
     * The draw_func may pass through the given previously computed surface
     * or change it as needed. This surface will be buffered internally.
     *
     * Passing null as draw_func will destroy the internal background buffer.
     *
     * @param draw_data_func function which creates/changes the background surface
     * @return the background surface of this item which may not be changed
     */
    public Surface ? get_background_surface (int width, int height, Surface model, DrawDataFunc<DockItem>? draw_data_func) {
      return background_buffer.get_surface<DockItem> (width, height, model, (DrawFunc<DockItem>) internal_get_background_surface, (DrawDataFunc<DockItem>) draw_data_func);
    }

    [CCode (instance_pos = -1)]
    Surface ? internal_get_background_surface (int width, int height, Surface model, DrawDataFunc<DockItem>? draw_data_func) {
      if (draw_data_func == null)
        return null;

      return draw_data_func (width, height, model, this);
    }

    /**
     * Returns the foreground surface for this item.
     *
     * The draw_func may pass through the given previously computed surface
     * or change it as needed. This surface will be buffered internally.
     *
     * Passing null as draw_func will destroy the internal foreground buffer.
     *
     * @param draw_data_func function which creates/changes the foreground surface
     * @return the background surface of this item which may not be changed
     */
    public Surface ? get_foreground_surface (int width, int height, Surface model, DrawDataFunc<DockItem>? draw_data_func) {
      if (draw_data_func == null) {
        foreground_surface = null;
        return null;
      }

      if (foreground_surface != null
          && foreground_surface.Width == width && foreground_surface.Height == height)
        return foreground_surface;

      foreground_surface = draw_data_func (width, height, model, this);

      return foreground_surface;
    }

    /**
     * Returns a copy of the surface for this item.
     *
     * It will trigger an internal redraw if the requested size
     * isn't matching the cache.
     *
     * @param width width of the icon surface
     * @param height height of the icon surface
     * @param model existing surface to use as basis of new surface
     * @return the copied surface for this item
     */
    public Surface get_surface_copy (int width, int height, Surface model) {
      return get_surface (width, height, model).copy ();
    }

    /**
     * Draws the item's icon onto a surface.
     *
     * @param surface the surface to draw on
     */
    protected virtual void draw_icon (Surface surface) {
      Cairo.Surface? icon = null;
      Gdk.Pixbuf? pbuf = ForcePixbuf;
      if (pbuf == null) {
        double x_scale = 1.0, y_scale = 1.0;
        surface.Internal.get_device_scale (out x_scale, out y_scale);
        icon = DrawingService.load_icon_for_scale (Icon, surface.Width, surface.Height, (int) double.max (x_scale, y_scale));
        if (icon != null)
          icon.set_device_scale (1.0, 1.0);
      } else {
        pbuf = DrawingService.ar_scale (pbuf, surface.Width, surface.Height);
      }

      unowned Cairo.Context cr = surface.Context;

      if (pbuf != null) {
        Gdk.cairo_set_source_pixbuf (cr, pbuf, (surface.Width - pbuf.width) / 2, (surface.Height - pbuf.height) / 2);
        cr.paint ();
      } else if (icon != null) {
        cr.set_source_surface (icon, 0, 0);
        cr.paint ();
      } else {
        warn_if_reached ();
      }
    }

    /**
     * Draws a placeholder icon onto a surface.
     * This method should be considered time-critical!
     * Make sure to only use simple drawing routines, and do not rely on external resources!
     *
     * @param surface the surface to draw on
     */
    protected virtual void draw_icon_fast (Surface surface) {
      unowned Cairo.Context cr = surface.Context;
      var width = surface.Width;
      var height = surface.Height;
      var radius = width / 2 - 1;

      var line_width_half = 1;

      cr.move_to (radius, line_width_half);
      cr.arc (radius + line_width_half, radius + line_width_half, radius, 0, 2 * Math.PI);
      cr.close_path ();

      cr.set_source_rgba (1, 1, 1, 0.2);
      cr.set_line_width (2 * line_width_half);
      cr.stroke_preserve ();

      var rg = new Cairo.Pattern.radial (width / 2, height, height / 8, width / 2, height, height);
      rg.add_color_stop_rgba (0, 0, 0, 0, 0.6);
      rg.add_color_stop_rgba (1, 0, 0, 0, 0.3);

      cr.set_source (rg);
      cr.fill ();
    }

    /**
     * Check the validity of this item.
     *
     * @return Whether or not this item is valid for the .dockitem given
     */
    public virtual bool is_valid () {
      return launcher_exists || Prefs.Launcher == "";
    }

    /**
     * Copy all property value of this dockitem instance to target instance.
     *
     * @param target the dockitem to copy the values to
     */
    public void copy_values_to (DockItem target) {
      (unowned ParamSpec)[] properties = get_class ().list_properties ();

      foreach (unowned ParamSpec prop in properties) {
        // Skip non-copyable properties to avoid warnings
        if ((prop.flags & ParamFlags.WRITABLE) == 0
            || (prop.flags & ParamFlags.CONSTRUCT_ONLY) != 0)
          continue;

        unowned string name = prop.get_name ();

        // Do not copy these
        if (name == "Container")
          continue;

        var type = prop.value_type;
        var val = Value (type);
        get_property (name, ref val);
        target.set_property (name, val);
      }
    }
  }
}
