#!/usr/bin/ruby

# This is a part of the external WebSearch applet for Cairo-Dock
#
# Author: Eduardo Mucelli Rezende Oliveira
# E-mail: edumucelli@gmail.com or eduardom@dcc.ufmg.br
#
# 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.

# 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.

# This applet provides an interface to some search engines such as
# Google, Bing, Teoma, Yahoo!, Youtube, Flickr, Wikipedia, and ImageShack.
#  To choose the search engine you can
#     (1) Right-click on the main icon -> WebSearch -> (Choose the engine)
#     (2) Right-click -> Configure this applet -> Configuration -> Search engine
#     (3) Scroll up or down over the icon (applicable only for the first search)
#   You can search in two ways
#    (1) Middle-click on the main icon
#    (2) Left-click on main icon (right after choosing a new engine)
#    Type your query and validate. Each result will be shown as an sub-icon.
#   Left-click to open the the result in the default web browser
#    Middle-click on the sub-icon of any result to show its description
#    Scroll up to fetch the next results
#   Scroll down to fetch the previous results
#   Left-click on the main icon to show search stats

%w{rubygems open-uri nokogiri dbus parseconfig launchy}.each { |x| require x }    # requirements

class Array
  def fifth;self[4];end                                                           # defining the method "fifth" just for code readability
  def third;self[2];end                                                           # defining the method "third" just for code readability
end

class String
  def to_b;"true" == downcase;end                                                 # string to boolean
  def starts_with?(prefix)
    prefix = prefix.to_s
    self[0, prefix.length] == prefix
  end
end

module WebSearch

  def self.name
    File.basename(Dir.getwd)                                                      # applet's name, the same name as the directory
  end

  def self.start
    bus = DBus::SessionBus.instance                                               # TODO: a module to encapsulate DBus-Dock connection
    applet_service = bus.service("org.cairodock.CairoDock")
    applet_path = "/org/cairodock/CairoDock/#{WebSearch.name}"                    # caminho onde o objeto está guardado no bus

    applet_object = applet_service.object(applet_path)
    applet_object.introspect
    applet_object.default_iface = 'org.cairodock.CairoDock.applet'                # list of icons contained in our sub-dock, or in our desklet

    applet_sub_icons_object = applet_service.object("#{applet_path}/sub_icons")
    applet_sub_icons_object.introspect
    applet_sub_icons_object.default_iface = 'org.cairodock.CairoDock.subapplet'   # list of icons contained in our sub-dock, or in our desklet

    applet = Applet.new applet_object, applet_sub_icons_object
    applet.start
    loop = DBus::Main.new
    loop << bus
    loop.run
  end

  class Applet

    require './lib/Engine.rb'
    require './lib/History.rb'

    attr_accessor :engine, :engines, :query, :history,
                  :number_of_fetched_links, :number_of_displayed_links, :page_of_displayed_links,
                  :show_current_page, :show_description_instead_url, :show_thumbnail_preview,
                  :scroll_engine_index, :show_logo_result

    DialogActiveTime = 5                                                          # time in seconds that the dialog window will be active

    def initialize applet, sub_icons
      self.query = ""
      self.engines = Engines.list                                                 # ./lib/Engines.rb
      self.history = History.entries                                              # ./lib/History.rb
      self.engine = Engine.new
      self.scroll_engine_index = 0                                                # current index when scrolling through search engines
      @icon = applet
      @sub_icons = sub_icons
      reset_search_settings
      set_configuration_parameters
    end

    def set_configuration_parameters
      conf = ParseConfig.new(File.expand_path("~/.config/cairo-dock/current_theme/plug-ins/#{WebSearch.name}/#{WebSearch.name}.conf"))
      # for parameters within a list, the value is the position in the options list, not the value by itself
      self.number_of_fetched_links = [10, 20, 30, 50, 100].at(conf.params['Configuration']['number of fetched links'].to_i)
      self.engine.name = self.engines.at(conf.params['Configuration']['engine'].to_i)
      inform_current_search_engine                        # inform in bottom of the icon what is the new engine    
      self.number_of_displayed_links = conf.params['Configuration']['number of displayed links'].to_i # number of sub-icons to be shown
      self.show_current_page = conf.params['Configuration']['show current page'].to_b
      self.show_description_instead_url = conf.params['Configuration']['show description instead url'].to_b
      self.show_thumbnail_preview = conf.params['Configuration']['show thumbnail preview'].to_b
      self.show_logo_result = conf.params['Configuration']['show logo result'].to_b
    end
  
    def start
      verify_user_action
    end

    # Signal handling
    def verify_user_action
      @icon.on_signal("on_build_menu") do |param|                                 # right click signal
        action_on_build_menu
      end
      @icon.on_signal("on_menu_select") do |selected_menu|
        action_on_menu_select selected_menu
      end
      @icon.on_signal("on_answer") do |answer|
        action_on_answer answer
      end
      @icon.on_signal("on_scroll") do |scroll_up|                                 # when the user scroll the mouse up or down on the icon
        action_on_scroll scroll_up                                                # scroll down param = false, scroll up param = true
      end
      @icon.on_signal("on_middle_click") do |param|
        ask_for_search_query
      end
      @icon.on_signal("on_click") do |param|
        action_on_click
      end
      @icon.on_signal("on_reload_module") do |config_has_changed|
        action_on_reload_module config_has_changed
      end
      @sub_icons.on_signal("on_click_sub_icon") do |param, sub_icon_id|
        action_on_click_sub_icon sub_icon_id
      end
      @sub_icons.on_signal("on_middle_click_sub_icon") do |sub_icon_id|
        action_on_middle_click_sub_icon sub_icon_id
      end
    end
        
        # Building context menu
    def action_on_build_menu
      begin
        @icon.AddMenuItems(build_menu_for_engines)
        @icon.AddMenuItems(build_menu_for_history)
      rescue NoMethodError                            # Cairo-Dock < 2.1.4-0beta0
        WebSearch.log "AddMenuItems method is not available"
        @icon.PopulateMenu(self.engines)
      end
    end

    def build_menu_for_engines
      items = []
      self.engines.each_with_index do |engine, i|                                 # each property must do be string and never use Symbol
        item = {}
        item['type'] = 0
        item['label'] = engine
        item['menu'] = 1
        item['id'] = i
        item['icon'] = engine.icon
        items.push item
      end
      items
    end

    def build_menu_for_history
      # The engines' index (id) of the WebSearch sub-menu ranges from 0 to self.engines.size-1 consequently, the first id of
      # the history menu is the self.engines.size. Even if the number of engines change, the self.engine size will keep track of it
      history_sub_menu_index = self.engines.size
      history_sub_menu_icon = File.expand_path("./images/data/history.png")
      history_sub_menu = [{'type' => 1, 'label' => 'History', 'menu' => 0, 'id' => history_sub_menu_index, 'icon' => history_sub_menu_icon}]
      history_sub_menu_entry_index = history_sub_menu_index                       # subsequent indexes are used in the history entries
      self.history.each do |entry|                                                # construct the history sub-menu entries
        item = {}
        item['type'] = 0
        item['label'] = entry.chop
        item['menu'] = history_sub_menu_index
        history_sub_menu_entry_index += 1
        item['id'] = history_sub_menu_entry_index
        item['icon'] = history_sub_menu_icon
        history_sub_menu.push item
      end
      history_sub_menu
    end

    def ask_for_search_query
      @icon.AskText("Search for:", "#{self.query}")                               # the value in the text field is the previous used term
    end

    def action_on_answer answer
      unless answer.empty?
        reset_search_settings unless self.query.empty?
        History.save_new_entry answer                                             # append (if necessary) the query term in the history file
        self.history = History.entries                                            # refresh local entries of the historyu
        self.query = answer
        begin
          self.engine = self.engine.connect                                       # only when the fetch is imminent the engine connection occurs
        rescue Exceptions::UnknownEngineException => e
          WebSearch.log e.message
        else
          fetch_next_resulting_page
        end
      end
    end

    def reset_search_settings
      self.engine.links =[]                                                       # .clear cant be used yet
      self.engine.stats = ""
      self.page_of_displayed_links = 0                                            # current pagination of displayed links
      Link.reset_next_id
      ThumbnailedLink.reset_next_image_id
      @sub_icons.RemoveSubIcon("any")
    end

    def action_on_click_sub_icon sub_icon_id
      url = self.engine.links.at(sub_icon_id.to_i-1).url
      unless url.starts_with?("http://")
        Launchy.open("http://#{url}")
      else
        Launchy.open url
      end
    end

    def action_on_middle_click_sub_icon sub_icon_id
      text = ""
      if self.show_description_instead_url                                        # sub-icons are entitled by description ...
        text = self.engine.links.at(sub_icon_id.to_i-1).url                       # so URL will be shown in dialog
      else                                                                        # sub-icons are entitled by url ...
        text = self.engine.links.at(sub_icon_id.to_i-1).description               # so description will be shown in dialog
      end          
      @icon.ShowDialog(text, DialogActiveTime)
    end

    def action_on_click
      if self.engine.stats.empty?
        ask_for_search_query
      else
        @icon.ShowDialog(self.engine.stats, DialogActiveTime) 
      end
    end

    # There is a sequential index in the context menu and it is divided in two sections
    #   Section I - Indexes for search engines ranging from 0 to self.engines.size - 1
    #   Section II - Indexes for history terms' entries ranging from self.engines.size + 1 to max number of entries in the history (NumberOfEntries see ./lib/History.rb)
    #   Note - The index self.engines.size is the one used to place the "History" label actually creating the sub-menu. See first lines in  build_menu_for_history method
    # This method treats both the selection of search engine, as trigger a search with a term coming from the history
    def action_on_menu_select selected_menu_index
      # an index that came from a click in one of the engines from WebSearch sub-menu
      if selected_menu_index < self.engines.size
        switch_search_engine selected_menu_index
      else    # user can be only clicked on the History sub-menu
        # shift the history menu ids to make them range from 0 to self.history-1 (History.entries from ./lib/History.rb)
        history_entry_term = self.history[selected_menu_index - self.engines.size - 1]
        action_on_answer history_entry_term
      end
    end

    def action_on_reload_module config_has_changed
      set_configuration_parameters if config_has_changed
    end

    # Scrolling can switch the search engine, or fetch another resulting page
    def action_on_scroll scroll_up
      if self.query.empty?                                                        # before the first query it is possible scroll through engines
        if scroll_up
          switch_search_engine self.scroll_engine_index +=1                       # drawback: user scrolls a lot for up/down and this variable
        else                                                                      # gets a value far from (0..self.engines.size-1) limits.
          switch_search_engine self.scroll_engine_index -=1                       # user need to scroll back a lot to get in these limits again
        end
      else                                                                        # later the first query scroll through the resulting pages
        if scroll_up
          fetch_next_resulting_page
        else
          fetch_previous_resulting_page
        end
      end
    end

    def switch_search_engine index
      index = 0 if index < 0                                                      # keep the lower limit
      index = self.engines.size - 1 if index > self.engines.size - 1              # keep the upper limit
      self.engine.name = self.engines.at(index)
      reset_search_settings                                                       # clean the previous search when choosing a new one
      inform_current_search_engine                                                # inform in the bottom of the icon what is the new engine
    end

    def fetch_next_resulting_page
      offset = self.page_of_displayed_links * self.number_of_displayed_links      # the position of the first link in the self.engine.links array
      if self.engine.links.size <= offset                                         # user already scrolled by the fetched links, fetch more
        inform_start_of_waiting_process
        # some engines use the concept of offset which is the first index of an interval of links/images to be shown
        # but there is those that use a sequential page (1,2,3, ...) which has an amount of links/images, etc
        if Engines.paginated_by_page?(self.engine.name)                           # see ./lib/Engines.rb
          self.engine.links = self.engine.retrieve_links(self.query, self.page_of_displayed_links + 1)
        else                                  # paginated by offset
          self.engine.links = self.engine.retrieve_links(self.query, offset)
        end
        inform_end_of_waiting_process
        end
      self.page_of_displayed_links += 1                                           # sequential page identification, lets go to the next
      sub_icon_list = construct_sub_icon_list(offset)
      refresh_sub_icon_list (sub_icon_list)
      inform_current_page
    end

    # Since the previous results are already stored in self.engine.links, it is necessary just to 
    # select the its interval that starts with the first link of the previous page.
    # An easier approach would be to query the engine again with page-1 but it would result
    # more queries to the page, consequently it has some drawbacks such as increasing the 
    # probability of forbidden mechanized access, more bandwith, etc.
    def fetch_previous_resulting_page
      if self.page_of_displayed_links > 1                                         # there is no previous page from the first one
        self.page_of_displayed_links -= 1                                         # one page back
        inicio = (self.page_of_displayed_links-1) * self.number_of_displayed_links  # the first position of the link in the previous page
        sub_icon_list = construct_sub_icon_list(inicio)
        refresh_sub_icon_list (sub_icon_list)
      end
      inform_current_page
    end

    # Construct the menu using a set of fetched links
    # Links can have thumbnails
    def construct_sub_icon_list inicio
      sub_icon_list =[]
      inform_start_of_waiting_process
      threads =[]
      self.engine.links[inicio, self.number_of_displayed_links].each do |link|    # first let's download the thumbs if necessary
        if link.instance_of?(ThumbnailedLink) and not link.downloaded_thumb?      # class that provides thumbs and a not yet downloaded thumb
          if self.show_thumbnail_preview                                          # user want to see thumbs, so let's get it
            threads << Thread.new {
              link.download_thumbnail
            }
          end
        end
      end
      threads.each {|t| t.join}
      self.engine.links[inicio, self.number_of_displayed_links].each do |link|    # later, get the rest of sub-icons data
        if self.show_description_instead_url
          sub_icon_list << link.description                                       # user prefer see description with the sub-icon
        else
          sub_icon_list << link.shortened_url                                     # user prefer see shortened url with the sub-icon
        end
        if not link.instance_of?(ThumbnailedLink) and self.show_logo_result
          link.icon = File.expand_path("./images/data/#{self.engine.name}.png")
        end
        sub_icon_list << link.icon                                                # the icon
        sub_icon_list << link.id.to_s                                             # the sequential id
      end
      inform_end_of_waiting_process
      sub_icon_list
    end

    def refresh_sub_icon_list sub_icon_list
      @sub_icons.RemoveSubIcon("any")                                             # remove all rendered sub-icons
      @sub_icons.AddSubIcons(sub_icon_list)
    end

    def inform_start_of_waiting_process
      @icon.SetQuickInfo("...")
    end

    def inform_end_of_waiting_process
      @icon.SetQuickInfo("")
    end

    def inform_current_page
      if self.show_current_page
        @icon.SetQuickInfo("#{self.page_of_displayed_links}")
      else
        @icon.SetQuickInfo("")
      end
    end
  
    # Inform in the bottom of the icon what is new search engine
    def inform_current_search_engine
      @icon.SetQuickInfo(self.engine.name)
    end
  end

  def self.log(msg)
    $stderr.puts "WEBSEARCH_DEBUG: #{msg}"
  end

end

WebSearch.start
