# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Kengo Tateishi <embrace.ddd.flake.peace@gmail.com>
# https://github.com/tkengo/weechat-url-hinter
#
# 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 3 of the License, or
# (at your option) any later version.
#
# ---------------------------------------------------------------------
#
# Url hinter is a plugin that open a url on weehcat buffer without
# touching mouse.
#
# This plugin is available in only Mac OSX.
#
# Usage
# 1. Type '/url_hinter' command on the input buffer of Weechat.
# 2. Then, this plugin searches url strings such as 'http://...' or
#    'https://...'
# 3. If urls are found, they are highlighted and give hint key to
#    the url.
# 4. When you type a hint key, open the url related to hint key
#    in your default browser.
#
# see also
# https://github.com/tkengo/weechat-url-hinter/blob/master/README.md
#
# v0.3 : add option "launcher"

require 'singleton'

#
# Register url-hinter plugin to weechat and do initialization.
#
def weechat_init
  Weechat.register('url_hinter', 'Kengo Tateish', '0.41', 'GPL3', 'Open an url in the weechat buffer to type a hint', '', '')
  Weechat.hook_command(
    'url_hinter',
    'Search url strings, and highlight them, and if you type a hint key, open the url related to hint key.',
    'continuous|first',
    "continuous | Continue hint mode even if selected url is opend.\n" +
    'first      | Open a url that appears first on the current buffer.',
    '',
    'launch_url_hinter',
    ''
  );
    option = 'launcher'
    if Weechat.config_is_set_plugin(option) == 0
      Weechat.config_set_plugin(option, 'open')
    end
    Weechat.config_get_plugin(option)
  Weechat::WEECHAT_RC_OK
end

#
# Callback method that invoked when input text in buffer was changed.
# Search url by the input text from Hint. If a url is found, open it.
#
def open_hint_url(data, signal, buffer_pointer)
  buffer = Buffer.new(buffer_pointer)
  text = buffer.input_text

  if Hint.instance.has_key?(text)
    Hint.instance.reserve(text)
    buffer.input_text = ''

    unless GlobalResource.continuous
      Hint.instance.open_all_reserved_url
      reset_hint_mode
    end
  end

  Weechat::WEECHAT_RC_OK
end

#
# Callback method that invoked when key was pressed in buffer.
# If enter key is pressed, open all reserved urls.
#
def key_pressed_callback(data, signal, key)
  if key == "\x01M" && Hint.instance.any?
    Hint.instance.open_all_reserved_url
    reset_hint_mode
  end

  Weechat::WEECHAT_RC_OK
end

#
# Launch url-hinter.
#
# Type '/url_hinter' on the input buffer of Weechat, and then this plugin searches
# strings like 'http://...' or 'https://...' in the current buffer and highlights it.
#
def launch_url_hinter(data, buffer_pointer, argv)
  buffer = Buffer.new(buffer_pointer)

  reset_hint_mode and return Weechat::WEECHAT_RC_OK if Hint.instance.any?
  return Weechat::WEECHAT_RC_OK unless buffer.has_url_in_display?
  open_url(buffer.first_url) and return Weechat::WEECHAT_RC_OK if argv == 'first'

  Hint.instance.set_target(buffer)

  messages = {}
  buffer.own_lines.each do |line|
    messages[line.data_pointer] = line.message.dup
    new_message = line.remove_color_message
    line.urls.each do |url|
      hint_key = "[#{Hint.instance.add(url, line)}]"
      new_message.gsub!(url, Color.yellow + hint_key + Color.red + url[hint_key.length..-1].to_s + Color.blue)
    end
    line.message = Color.blue + new_message + Color.reset
  end

  GlobalResource.messages = messages
  GlobalResource.continuous = argv == 'continuous'
  GlobalResource.hook_pointer1 = Weechat.hook_signal('input_text_changed', 'open_hint_url', '')
  GlobalResource.hook_pointer2 = Weechat.hook_signal('key_pressed', 'key_pressed_callback', '')
  Weechat::WEECHAT_RC_OK
end

#
# Clear hints and reset hook.
#
def reset_hint_mode
  Hint.instance.clear
  GlobalResource.messages.each {|pointer, message| Weechat.hdata_update(Weechat.hdata_get('line_data'), pointer, { 'message' => message }) }
  Weechat.unhook(GlobalResource.hook_pointer1)
  Weechat.unhook(GlobalResource.hook_pointer2)
end

#
# open specified url
#
def open_url(url)
  launcher = Weechat.config_get_plugin('launcher')

  Weechat.hook_process("#{launcher} #{url}", 10000, '', '')
end

#----------------------------
# Custome classes
#----------------------------

class Hint
  include Singleton

  def initialize
    clear
  end

  def set_target(buffer)
    @buffer = buffer
    @url_count = @buffer.url_count
  end

  def clear
    @urls = {}
    @open_target_urls = []
    @lines = {}
    @hint_key_index = 0
  end

  def any?
    @urls.any?
  end

  def add(url, line)
    hint_key = next_hint_key
    @urls[hint_key] = url
    @lines[hint_key] = line
    hint_key
  end

  def reserve(key)
    line = @lines.delete(key)
    line.message = line.message(hdata: true).gsub("[#{key}]", "[#{'*' * key.length}]")
    @open_target_urls << @urls.delete(key)
  end

  def open_all_reserved_url
    launcher = Weechat.config_get_plugin('launcher')
    Weechat.hook_process("#{launcher} #{@open_target_urls.join(' ')}", 10000, '', '') if @open_target_urls.any?
  end

  def has_key?(key)
    @urls.has_key?(key)
  end

  private

  def get_hint_keys
    option = 'hintkeys'
    if Weechat.config_is_set_plugin(option) == 0
      Weechat.config_set_plugin(option, 'jfhkgyuiopqwertnmzxcvblasd')
    end
    Weechat.config_get_plugin(option)
  end

  def next_hint_key
    hint_keys = get_hint_keys()
    if @url_count > hint_keys.length
      key1 = hint_keys[@hint_key_index / hint_keys.length]
      key2 = hint_keys[@hint_key_index % hint_keys.length]
      hint_key = key1 + key2
    else
      hint_key = hint_keys[@hint_key_index]
    end

    @hint_key_index += 1
    hint_key
  end
end

class GlobalResource
  class << self
    attr_accessor :hook_pointer1, :hook_pointer2, :messages, :continuous
  end
end

#----------------------------
# Wrapper of weechat objects.
#----------------------------

#
# Wrapper of weechat color object.
#
class Color
  class << self
    def method_missing(method_name)
      Weechat.color(method_name.to_s)
    end
  end
end

#
# Wrapper of weechat hdata window.
#
class Window
  class << self
    def current
      Window.new(Weechat.current_window)
    end
  end

  def initialize(pointer)
    @pointer = pointer
  end

  def chat_height
    Weechat.hdata_integer(Weechat.hdata_get('window'), @pointer, 'win_chat_height')
  end
end

#
# Wrapper of weechat hdata buffer.
#
class Buffer
  def initialize(pointer)
    @pointer = pointer
  end

  def own_lines
    own_lines_pointer = Weechat.hdata_pointer(Weechat.hdata_get('buffer'), @pointer, 'own_lines')
    @own_lines ||= Lines.new(own_lines_pointer)
  end

  def input_text
    Weechat.buffer_get_string(@pointer, 'input')
  end

  def input_text=(text)
    Weechat.buffer_set(@pointer, 'input', text)
  end

  def url_count
    own_lines.inject(0){|result, line| result + line.urls.count }
  end

  def has_url_in_display?
    !own_lines.find(&:has_url?).nil?
  end

  def first_url
    own_lines.find(&:has_url?).urls.last
  end
end

#
# Wrapper of weechat hdata lines.
#
class Lines
  include Enumerable

  def initialize(pointer)
    @pointer = pointer
  end

  def first_line
    first_line_pointer = Weechat.hdata_pointer(Weechat.hdata_get('lines'), @pointer, 'first_line')
    Line.new(first_line_pointer)
  end

  def last_line
    last_line_pointer = Weechat.hdata_pointer(Weechat.hdata_get('lines'), @pointer, 'last_line')
    Line.new(last_line_pointer)
  end

  def count
    Weechat.hdata_integer(Weechat.hdata_get('lines'), @pointer, 'lines_count')
  end

  def each
    window_height = Window.current.chat_height
    line          = last_line
    index         = 0

    while true
      yield(line)

      index += 1 if line.displayed?
      break if !(line = line.prev) || index >= window_height
    end
  end
end

#
# Wrapper of weechat hdata line and line_data.
#
class Line
  attr_reader :data_pointer

  def initialize(pointer)
    @pointer = pointer
    @data_pointer = Weechat.hdata_pointer(Weechat.hdata_get('line'), @pointer, 'data')
  end

  def message(options = {})
    if options[:hdata]
      @message = Weechat.hdata_string(Weechat.hdata_get('line_data'), @data_pointer, 'message').to_s
    else
      @message ||= Weechat.hdata_string(Weechat.hdata_get('line_data'), @data_pointer, 'message').to_s
    end
  end

  def message=(new_message)
    Weechat.hdata_update(Weechat.hdata_get('line_data'), @data_pointer, { 'message' => new_message })
  end

  def remove_color_message
    ret = Weechat.string_remove_color(message.dup, '')
    ret.force_encoding("UTF-8")
    return ret
  end

  def next
    next_line_pointer = Weechat.hdata_pointer(Weechat.hdata_get('line'), @pointer, 'next_line')
    Line.new(next_line_pointer) unless next_line_pointer.to_s.empty?
  end

  def prev
    prev_line_pointer = Weechat.hdata_pointer(Weechat.hdata_get('line'), @pointer, 'prev_line')
    Line.new(prev_line_pointer) unless prev_line_pointer.to_s.empty?
  end

  def displayed?
    Weechat.hdata_char(Weechat.hdata_get('line_data'), @data_pointer, 'displayed').to_s == '1'
  end

  def has_url?
    !/https?:\/\/[^ 　\(\)\r\n]*/.match(remove_color_message).nil?
  end

  def urls
    remove_color_message.encode("UTF-8", invalid: :replace, undef: :replace).scan(/https?:\/\/[^ 　\(\)\r\n]*/).uniq
  end
end
