# -*- coding: utf-8 -*-

module Plugin::OpenimgGtk
  class Window < Gtk::Window
    attr_reader :photo

    def initialize(photo, next_opener)
      super()
      @photo = photo
      @image_surface = loading_surface
      @next_opener = next_opener
      window_settings
      ssc :key_release_event do |_, ev|
        destroy if ::Gtk.keyname([ev.keyval, ev.state]) == 'Escape'
      end
      ssc(:destroy, &:destroy)
    end

    def start_loading
      Thread.new {
        Plugin.filtering(:openimg_pixbuf_from_display_url, photo, nil, nil)
      }.next { |_, pixbufloader, complete_promise|
        if pixbufloader.is_a?(GdkPixbuf::PixbufLoader)
          pixbufloader.ssc(:area_updated, self, &pixbufloader_area_update_handler)
          complete_promise.next {
            progress(pixbufloader.pixbuf)
          }.trap { |exception|
            error exception
            @image_surface = error_surface
            redraw(repaint: true)
          }.next do
            pixbufloader.close
          end
        else
          warn "cant open: #{photo}"
          @image_surface = error_surface
          redraw(repaint: true)
        end
      }.trap do |exception|
        error exception
        @image_surface = error_surface
        redraw(repaint: true)
      end
      self
    end

    private

    def pixbufloader_area_update_handler
      rect = nil
      ->(pixbufloader, *xywh) do
        new_rect = PartialUpdateRect.new(*xywh)
        atomic do
          if rect
            rect = rect.expand(new_rect)
          else
            rect = new_rect
            Delayer.new do
              atomic do
                progress(pixbufloader.pixbuf, partial: rect)
                rect = nil
              end
            end
          end
        end
        true
      end
    end

    def window_settings
      set_title(photo.perma_link.to_s)
      set_role('mikutter_image_preview'.freeze)
      set_type_hint(:dialog)
      set_default_size(*default_size)
      add(Gtk::Box.new(:vertical).pack_start(w_toolbar)
                                 .pack_start(w_wrap, fill: true, expand: true))
    end

    def redraw(repaint: true)
      return if w_wrap.destroyed?
      gdk_window = w_wrap.window
      return unless gdk_window
      win_rect = PartialUpdateRect.new(*gdk_window.geometry)
      return if win_rect.area == 0
      gdk_window.create_cairo_context.save do |context|
        if repaint
          context.set_source_color(Cairo::Color::BLACK)
          context.paint
        end
        *point, rate = win_rect.inscribed(@image_surface)
        context.translate(*point)
        context.scale(rate, rate)
        context.set_source(Cairo::SurfacePattern.new(@image_surface))
        context.paint
      end
    rescue StandardError => exception
      error exception
    end

    # @param [PartialUpdateRect,nil] partial 更新範囲(nilの場合は全範囲)
    def progress(pixbuf, partial: nil)
      return unless pixbuf
      context = nil
      size_changed = false
      unless (@image_surface.width == pixbuf.width) && (@image_surface.height == pixbuf.height)
        size_changed = true
        @image_surface = Cairo::ImageSurface.new(pixbuf.width, pixbuf.height)
        context = Cairo::Context.new(@image_surface)
        context.save do
          context.set_source_color(Cairo::Color::BLACK)
          context.paint
        end
      end
      context ||= Cairo::Context.new(@image_surface)
      context.save do
        case partial
        in x:, y:, width: w, height: h
          context.set_source_pixbuf(pixbuf)
          context.rectangle(x, y, w, h)
          context.fill
        in nil
          context.set_source_color(Cairo::Color::BLACK)
          context.paint
          context.set_source_pixbuf(pixbuf)
          context.paint
        end
      end
      redraw(repaint: !partial || size_changed)
    end

    #
    # === Widgetたち
    #

    def w_wrap
      @w_wrap ||= ::Gtk::DrawingArea.new.tap do |w|
        w.ssc(:size_allocate, &gen_wrap_size_allocate)
        w.ssc(:draw, &gen_wrap_expose_event)
      end
    end

    def w_toolbar
      @w_toolbar ||= ::Gtk::Toolbar.new.tap { |w| w.insert(w_browser, 0) }
    end

    def w_browser
      @w_browser ||= ::Gtk::ToolButton.new(
        icon_widget: Gtk::Image.new(pixbuf: Skin[:forward].pixbuf(width: 24, height: 24))
      ).tap do |w|
        w.ssc(:clicked, &gen_browser_clicked)
      end
    end

    #
    # === イベントハンドラ
    #

    def gen_browser_clicked
      proc do
        @next_opener.forward
        false
      end
    end

    def gen_wrap_expose_event
      proc do |_widget|
        redraw(repaint: true)
        true
      end
    end

    def gen_wrap_size_allocate
      last_size = nil
      proc do |widget|
        if widget.window && last_size != widget.window.geometry[2, 2]
          last_size = widget.window.geometry[2, 2]
          redraw(repaint: true)
        end
        false
      end
    end

    #
    # === その他
    #

    def default_size
      rect = case UserConfig[:openimg_window_size_reference]
             when :full
               screen
             when :mainwindow
               mainwindow = Plugin[:gtk3].widgetof(Plugin::GUI::Window.instance(:default))
               screen.get_monitor_geometry(screen.get_monitor(mainwindow.window))
             when :manual
               monitor = UserConfig[:openimg_window_size_reference_manual_num]
               max_monitor = screen.n_monitors
               if monitor > max_monitor
                 monitor = 0
               end
               screen.get_monitor_geometry(monitor)
             end

      @size || [rect.width * UserConfig[:openimg_window_size_width_percent].fdiv(100),
                rect.height * UserConfig[:openimg_window_size_height_percent].fdiv(100)]
    end

    def loading_surface
      Cairo::ImageSurface.from_png(Skin.get_path('loading.png'))
    end

    def error_surface
      Cairo::ImageSurface.from_png(Skin.get_path('notfound.png'))
    end
  end

  PartialUpdateRect = Data.define(:x, :y, :width, :height) do
    def area
      width * height
    end

    def expand(rect)
      PartialUpdateRect.new(
        x: [x, rect.x].min,
        y: [y, rect.y].min,
        width: [width, rect.width].max,
        height: [height, rect.height].max
      )
    end

    # _other_ を _self_ に内接するように拡大・縮小した場合の位置 _x_, _y_, 拡大率 _rate_ を返す
    # @param other [PartialUpdateRect] 中に入れる矩形
    # @return [[Numeric, Numeric, Numeric]] x, y, rate
    def inscribed(other)
      if (width * other.height) > (height * other.width)
        rate = height.fdiv(other.height)
        [(width - other.width * rate) / 2, 0, rate]
      else
        rate = width.fdiv(other.width)
        [0, (height - other.height * rate) / 2, rate]
      end
    end
  end
end
