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

require 'gtk3'
require 'cairo'

=begin rdoc
ナウい引用っぽく _Cairo::MiraclePainter_ のSubPartsとして別の _Message_ を表示するSubPartsを作るときの基底クラス。

= 使い方
このクラスを継承しましょう。
そして、以下のドキュメントを参考に、必要なメソッドをオーバライドします。
=end
class Gdk::SubPartsMessageBase < Gdk::SubParts
  extend Memoist

  DEFAULT_ICON_SIZE = 32
  WHITE = [1.0, 1.0, 1.0].freeze

  # SubPartsに表示する _Message_ 。
  # 複数表示可能なので、それらを上に表示されるものから順番に返す。
  # サブクラスで処理を実装すること。
  # このメソッドはサブパーツの描画中に何回も呼ばれるので、キャッシュなどで高速化に努めること。
  # ==== Return
  # _Array_ :: このSubParts上に表示する _Message_
  def messages
    [] end

  # :nodoc:
  memoize def score(message)
    Plugin[:gtk3].score_of(message)
  end

  # ヘッダの左の、Screen name、名前が表示されている場所に表示するテキスト。
  # オーバライドしなければ、 _message_ の投稿者のscreen nameと名前が表示される。
  # nilを返した場合、ヘッダは表示されない。この場合、ヘッダ右も表示されない。
  # ==== Args
  # [message] Message 表示するMessage
  # ==== Return
  # 次の3つの値またはnil（ヘッダ左を使用しない場合）
  # [String] 表示する文字列
  # [Pango::FontDescription] フォント情報
  # [Pango::Attribute] マークアップ情報
  def header_left_content(message)
    user = message.user
    if user.respond_to?(:idname)
      attr_list, text = Pango.parse_markup("<b>#{Pango.escape(user.idname)}</b> #{Pango.escape(user.name || '')}")
    else
      attr_list, text = Pango.parse_markup(Pango.escape(user.name || ''))
    end
    return text, header_left_font(message), attr_list
  end

  # ヘッダ左に使用するフォントを返す
  # ==== Args
  # [message] Message 表示するMessage
  # ==== Return
  # [Pango::FontDescription] フォント情報
  def header_left_font(message)
    default_font end

  # ヘッダの右の、タイムスタンプが表示されているところに表示するテキスト。
  # オーバーライドしなければ、 _message_ のタイムスタンプが表示される。
  # 表示される時に、 Pango.escape を通るので、この戻り値がエスケープを考慮する必要はないが、装飾を指定することはできない。
  # ==== Args
  # [message] Message 対象のMessage
  # ==== Return
  # [String] 表示する文字列。
  def header_right_text(message)
    now = Time.now
    if message[:created].year == now.year && message[:created].month == now.month && message[:created].day == now.day
      message[:created].strftime('%H:%M:%S'.freeze)
    else
      message[:created].strftime('%Y/%m/%d %H:%M:%S'.freeze) end end

  # Gdk::SubPartsMessageBase#header_right_text にマークアップを足した文字列を返す。
  # 通常は header_right_text をオーバライドするようにし、
  # テキストがエスケープされるのが問題になる場合は、こちらをオーバライドする。
  # nilを返した場合、ヘッダ右は表示されない。この場合、ヘッダ左のみが表示される。
  # ヘッダ自体を消す方法については、 Gdk::SubPartsMessageBase#header_left_text を参照
  # ==== Args
  # [message] Message 対象のMessage
  # ==== Return
  # 次の3つの値またはnil（ヘッダ右を使用しない場合）
  # [String] 表示する文字列
  # [Pango::FontDescription] フォント情報
  # [Pango::Attribute] マークアップ情報
  def header_right_content(message)
    attr_list, text = Pango.parse_markup("<span foreground=\"#999999\">#{Pango.escape(header_right_text(message))}</span>")
    return text, header_right_font(message), attr_list end

  # ヘッダ右に使用するフォントを返す
  # ==== Args
  # [message] Message 表示するMessage
  # ==== Return
  # [Pango::FontDescription] フォント情報
  def header_right_font(message)
    default_font end

  # SubParts内の _Message_ の左上に表示するバッジ。
  # サブクラスで処理を実装すること。
  # ==== Args
  # [message] Message
  # ==== Return
  # 以下の値のいずれか一つ
  # GdkPixbuf::Pixbuf :: _message_ の左上に表示するバッジ画像
  # nil :: バッジを表示しない
  def badge(message)
    nil end

  # 表示している _Message_ がクリックされた時、その _Message_ を引数に呼ばれる。
  # サブクラスで処理を実装すること。
  # ==== Args
  # [e] Gdk::EventButton クリックイベント
  # [message] Message クリックされた _Message_
  def on_click(e, message)
  end

  # SubParts内の _Diva::Model_ の背景色を返す
  # ==== Args
  # [model] Diva::Model
  # ==== Return
  # Array :: red, green, blueの配列。各要素は0.0..1.0の範囲。
  def background_color(model)
    color = Plugin.filtering(:message_bg_color, model, nil).last
    rgb(color || WHITE)
  end

  # SubParts内の _Message_ の枠の色を返す
  # ==== Args
  # [message] Message
  # ==== Return
  # Array :: red, green, blueの配列。各要素は0.0..1.0の範囲。
  def edge_color(message)
    [0.5]*3 end

  # GTK2のGtk::ColorとGTK3のGdk::RGBAをRGBの_Float_値に変換する
  def rgb(color)
    r, g, b = color
    return [r, g, b] if r.is_a? Float
    [r.fdiv(65536), g.fdiv(65536), b.fdiv(65536)].freeze
  end

  # アイコンのサイズを返す。
  # ==== Return
  # [Gdk::Rectangle] サイズ(px)。xとyは無視され、widthとheightのみが利用される
  # [nil] アイコンを表示しない
  def icon_size
    Gdk::Rectangle.new(0, 0, Gdk.scale(DEFAULT_ICON_SIZE), Gdk.scale(DEFAULT_ICON_SIZE))
  end

  # _message_ の本文のテキスト色を返す
  # ==== Args
  # [message] Message
  # ==== Return
  # Array :: red, green, blueの配列。各要素は0.0..1.0の範囲。
  def main_text_color(message)
    ([0,0,0]).map{ |c| c.to_f / 65536 } end

  # 本文使用するフォントを返す
  # ==== Args
  # [message] Message 表示するMessage
  # ==== Return
  # [Pango::FontDescription] フォント情報
  def main_text_font(message)
    default_font end

  # 本文の最大表示行数を返す。
  # この行数を超えた行は表示されない
  # ==== Args
  # [message] Message 表示するMessage
  # ==== Return
  # Fixnum 行数
  def text_max_line_count(message)
    3 end

  # :nodoc:
  memoize def default_font
    helper.font_description(UserConfig[:reply_text_font])
  end

  def margin
    Gdk.scale(@margin)
  end

  def edge
    Gdk.scale(@edge)
  end

  # Fixnum 枠線の太さ(px)
  def border_weight
    Gdk.scale(@border_weight)
  end

  # Fixnum バッジの半径(px)
  def badge_radius
    Gdk.scale(@badge_radius)
  end

  # :nodoc:
  def initialize(*args)
    super
    @margin, @edge, @border_weight, @badge_radius = 2, 8, 1, 6 end

  # :nodoc:
  def render_messages
    if not helper.destroyed?
      helper.queue_allocate
      helper.ssc(:clicked) { |_, ev|
        x, y = ev.x, ev.y
        ofsty = helper.mainpart_height
        helper.subparts.each { |part|
          break if part == self
          ofsty += part.height }
        if ofsty <= y and (ofsty + height) >= y
          my = 0
          messages.each { |m|
            my += message_height(m)
            if y <= ofsty + my
              on_click(ev, m)
              break end } end } end end

  # :nodoc:
  def render(context)
    if messages and not messages.empty?
      messages.inject(0) { |base_y, message|
        render_single_message(message, context, base_y) } end end

  # :nodoc:
  def height
    if helper.destroyed?
      0
    else
      messages&.sum(&method(:message_height)) || 0
    end
  end

  private

  def icon_width
    size = icon_size
    if size
      size.width
    else
      0 end end

  def icon_height
    size = icon_size
    if size
      size.height
    else
      0 end end

  def render_single_message(message, context, base_y)
    render_outline(message, context, base_y)
    _header_width, header_height = render_header(message, context, base_y)
    context.save do
      context.translate(margin + edge, margin + edge + base_y)
      render_icon(message, context)
      context.save do
        context.translate(icon_width + margin*2, header_height || 0)
        context.set_source_rgb(*main_text_color(message))
        pango_layout = main_message(message)
        if pango_layout.line_count <= text_max_line_count(message)
          context.show_pango_layout(pango_layout)
        else
          line_height = pango_layout.pixel_size[1] / pango_layout.line_count + pango_layout.spacing / Pango::SCALE
          context.translate(0, line_height*0.75)
          (0...text_max_line_count(message)).map(&pango_layout.method(:get_line)).each do |line|
            context.show_pango_layout_line(line)
            context.translate(0, line_height) end end end
      render_badge(message, context) end

    base_y + message_height(message) end

  def message_height(message)
    header_height = [0, *[header_left(message), header_right(message)].compact.map{|h|
                       h.pixel_size[1]}].max
    [icon_height, (header_height + main_message_height(message))].max + (margin + edge) * 2 end

  def main_message_height(message)
    pango_layout = main_message(message)
    result = pango_layout.pixel_size[1]
    if pango_layout.line_count <= text_max_line_count(message)
      result
    else
      (result / pango_layout.line_count) * text_max_line_count(message) + pango_layout.spacing/Pango::SCALE * 2 end end

  def header_left_context(message)
    PangoCairo::FontMap.default.create_context.tap do |context|
      context.set_font_description(header_left_font(message))
    end
  end

  # ヘッダ（左）のための Pango::Layout のインスタンスを返す
  def header_left(message, context=nil)
    text, _font, attr_list = header_left_content(message)
    if text
      layout = Pango::Layout.new(header_left_context(message))
      layout.attributes = attr_list if attr_list
      layout.text = text
      layout
    end
  end

  def header_right_context(message)
    PangoCairo::FontMap.default.create_context.tap do |context|
      context.set_font_description(header_right_font(message))
    end
  end

  # ヘッダ（右）のための Pango::Layout のインスタンスを返す
  def header_right(message, context=nil)
    text, _font, attr_list = header_right_content(message)
    if text
      layout = Pango::Layout.new(header_right_context(message))
      layout.attributes = attr_list if attr_list
      layout.text = text
      layout.alignment = Pango::Alignment::RIGHT
      layout
    end
  end

  def render_header(message, context, base_y)
    context.save do
      context.translate(icon_width + margin*2 + edge, margin + edge + base_y)
      context.set_source_rgb(0,0,0)
      hl_layout = header_left(message, context)
      if hl_layout
        context.show_pango_layout(hl_layout)
        hl_w, hl_h = hl_layout.pixel_size
        hr_layout = render_header_right(message, context, base_y, hl_w)
        if hr_layout
          [[hl_w, hr_layout.pixel_size[0]].max,
           [hl_h, hr_layout.pixel_size[1]].max]
        else
          [hl_w, hl_h] end end end end

  def render_header_right(message, context, base_y, header_left_width)
    header_w = width - icon_width - margin*3 - edge*2
    hr_layout = header_right(message, context)
    if hr_layout
      context.save do
        context.translate(header_w - hr_layout.pixel_size[0], 0)
        if header_left_width > header_w - hr_layout.pixel_size[0] - 20
          r, g, b = background_color(message)
          grad = Cairo::LinearPattern.new(-20, base_y, hr_layout.pixel_size[0] + 20, base_y)
          grad.add_color_stop_rgba(0.0, r, g, b, 0.0)
          grad.add_color_stop_rgba(20.0 / (hr_layout.pixel_size[0] + 20), r, g, b, 1.0)
          grad.add_color_stop_rgba(1.0, r, g, b, 1.0)
          context.rectangle(-20, 0, hr_layout.pixel_size[0] + 20, hr_layout.pixel_size[1])
          context.set_source(grad)
          context.fill() end
        context.show_pango_layout(hr_layout)
        hr_layout end end end

  def main_message_context
    PangoCairo::FontMap.default.create_context.tap do |context|
      context.set_font_description(default_font)
    end
  end

  # _message_ に対する _Pango::Layout_ を得る。
  # @params message [Diva::Model] 対象Message Model
  # @params _context [nil] 互換性のため
  def main_message(message, _context=nil)
    Pango::Layout.new(main_message_context).tap do |layout|
      layout.width = (width - icon_width - margin*3 - edge*2) * Pango::SCALE
      layout.attributes = description_attr_list(
        message,
        emoji_height: layout.context.font_description.forecast_font_size,
        pango_context: layout.context
      )
      layout.wrap = Pango::WrapMode::CHAR
      layout.text = plain_description(message)
      layout.context.set_shape_renderer do |c, shape, _|
        photo = shape.data
        if photo
          draw_area = layout.index_to_pos(shape.start_index)
          width = draw_area.width / Pango::SCALE
          height = draw_area.height / Pango::SCALE
          pixbuf = photo.load_pixbuf(width: width, height: height) do
            helper.queue_resize
          end
          x = draw_area.x / Pango::SCALE
          y = draw_area.y / Pango::SCALE
          c.translate(x, y)
          c.set_source_pixbuf(pixbuf)
          c.rectangle(0, 0, width, height)
          c.fill
        end
      end
    end
  end

  def render_outline(message, context, base_y)
    render_outline_floating(message, context, base_y) end

  # エッジの描画。
  # 影にblurを入れて、浮いているような感じに
  def render_outline_floating(message, context, base_y, radius: 4, blur: 4)
    x,y,w,h = edge, edge + base_y, width - edge*2, message_height(message) - edge*2
    context.save {
      context.pseudo_blur(blur) {
        context.fill {
          context.set_source_rgb(*edge_color(message))
          context.rounded_rectangle(x,y,w,h, radius) } }
      context.fill {
        context.set_source_rgb(*background_color(message))
        context.rounded_rectangle(x,y,w,h, radius) } } end

  # エッジの描画。
  # 細い線を入れる
  def render_outline_solid(message, context, base_y, radius: 4)
    context.save {
      x,y,w,h = edge, edge + base_y, width - edge*2, message_height(message) - edge*2
      #context.fill {
        context.rounded_rectangle(x,y,w,h, radius)
        context.set_source_rgb(*background_color(message))
        context.fill_preserve
        context.set_line_width(border_weight)
        context.set_source_rgb(*edge_color(message))
        context.stroke } end

  # エッジの描画。
  # 枠線なし
  def render_outline_flat(message, context, base_y, radius: 4)
    context.save {
      x,y,w,h = edge, edge + base_y, width - edge*2, message_height(message) - edge*2
      context.fill {
        context.set_source_rgb(*background_color(message))
        context.rounded_rectangle(x,y,w,h, radius) } } end

  def render_badge(message, context)
    render_badge_floating(message, context) end

  # バッジの描画。
  # 影にblurを入れて、浮いているような感じに
  def render_badge_floating(message, context)
    badge_pixbuf = badge(message)
    if badge_pixbuf
      context.save {
        context.pseudo_blur(4) {
          context.fill {
            context.set_source_rgb(*edge_color(message))
            context.circle(0, 0, badge_radius) } }
        context.fill {
          context.set_source_rgb(*background_color(message))
          context.circle(0, 0, badge_radius) } }
      context.translate(-badge_radius, -badge_radius)
      context.set_source_pixbuf(badge_pixbuf)
      context.paint end end

  # バッジの描画。
  # 細い線を入れる
  def render_badge_solid(message, context)
    badge_pixbuf = badge(message)
    if badge_pixbuf
      context.save {
        context.circle(0, 0, badge_radius)
        context.set_source_rgb(*background_color(message))
        context.fill_preserve
        context.set_source_rgb(*edge_color(message))
        context.set_line_width(border_weight)
        context.stroke }
      context.translate(-badge_radius, -badge_radius)
      context.set_source_pixbuf(badge_pixbuf)
      context.paint end end

  # バッジの描画。
  # 枠線を入れない
  def render_badge_flat(message, context)
    badge_pixbuf = badge(message)
    if badge_pixbuf
      context.fill {
        context.set_source_rgb(*background_color(message))
        context.circle(0, 0, badge_radius) }
      context.translate(-badge_radius, -badge_radius)
      context.set_source_pixbuf(badge_pixbuf)
      context.paint end end

  def render_icon(message, context)
    if icon_size
      context.set_source_pixbuf(main_icon(message))
      context.paint end end

  def main_icon(message)
    message.user.icon.load_pixbuf(width: icon_size.width, height: icon_size.width){ helper.queue_draw }
  end

  # 表示する際に本文に適用すべき装飾オブジェクトを作成する
  # ==== Return
  # Pango::AttrList 本文に適用する装飾
  def description_attr_list(message, attr_list: Pango::AttrList.new, emoji_height: 24, pango_context: nil)
    descent = 0
    if pango_context
      metrics = pango_context.get_metrics(pango_context.font_description, pango_context.language)
      descent = metrics.descent
    end
    height = emoji_height * Pango::SCALE
    y = -height + descent
    score(message).inject(0){|start_index, note|
      end_index = start_index + note.description.bytesize
      if UserConfig[:miraclepainter_expand_custom_emoji] && note.respond_to?(:inline_photo)
        end_index += -note.description.bytesize + 1
        rect = Pango::Rectangle.new(0, y, height, height)
        shape = Pango::AttrShape.new(rect, rect, note.inline_photo)
        shape.start_index = start_index
        shape.end_index = end_index
        attr_list.insert(shape)
      end
      end_index
    }
    attr_list
  end

  # Entityを適用したあとのプレーンテキストを返す。
  # Pangoの都合上、絵文字は1文字で表現する
  def plain_description(message)
    _plain_description[[message, UserConfig[:miraclepainter_expand_custom_emoji]]]
  end

  private memoize def _plain_description
    Hash.new do |h, k|
      message, expand_emoji = k
      h[k] = score(message).map{|note|
        if expand_emoji && note.respond_to?(:inline_photo)
          '.'
        else
          note.description
        end
      }.to_a.join
    end
  end
end
