require 'open-uri'
require 'digest/sha1'

require_dependency 'oneboxer/base'
require_dependency 'oneboxer/whitelist'
Dir["#{Rails.root}/lib/oneboxer/*_onebox.rb"].each {|f|
  require_dependency(f.split('/')[-2..-1].join('/'))
}

module Oneboxer
  extend Oneboxer::Base

  # keep reloaders happy
  unless defined? Oneboxer::Result
    Result = Struct.new(:doc, :changed) do
      def to_html
        doc.to_html
      end

      def changed?
        changed
      end
    end
  end

  Dir["#{Rails.root}/lib/oneboxer/*_onebox.rb"].sort.each do |f|
    add_onebox "Oneboxer::#{Pathname.new(f).basename.to_s.gsub(/\.rb$/, '').classify}".constantize
  end

  def self.default_expiry
    1.day
  end

  # Return a oneboxer for a given URL
  def self.onebox_for_url(url)
    matchers.each do |matcher|
      regexp = matcher.regexp
      klass = matcher.klass

      regexp = regexp.call if regexp.class == Proc
      return klass.new(url) if url =~ regexp
    end
    nil
  end

  # Retrieve the onebox for a url without caching
  def self.onebox_nocache(url)
    oneboxer = onebox_for_url(url)
    return oneboxer.onebox if oneboxer.present?

    whitelist_entry = Whitelist.entry_for_url(url)

    if whitelist_entry.present?
      # TODO - only download HEAD section
      # TODO - sane timeout
      # TODO - FAIL if for any reason you are downloading more that 5000 bytes
      page_html = open(url).read
      if page_html.present?
        doc = Nokogiri::HTML(page_html)

        if whitelist_entry.allows_oembed?
          # See if if it has an oembed thing we can use
          (doc/"link[@type='application/json+oembed']").each do |oembed|
            return OembedOnebox.new(oembed[:href]).onebox
          end
          (doc/"link[@type='text/json+oembed']").each do |oembed|
            return OembedOnebox.new(oembed[:href]).onebox
          end
        end

        # Check for opengraph
        open_graph = Oneboxer.parse_open_graph(doc)
        return OpenGraphOnebox.new(url, open_graph).onebox if open_graph.present?
      end
    end

    nil
  rescue OpenURI::HTTPError
    nil
  end

  # Parse URLs out of HTML, returning the document when finished.
  def self.each_onebox_link(string_or_doc)
    doc = string_or_doc
    doc = Nokogiri::HTML::fragment(doc) if doc.is_a?(String)

    onebox_links = doc.search("a.onebox")
    if onebox_links.present?
      onebox_links.each do |link|
        if link['href'].present?
          yield link['href'], link
        end
      end
    end

    doc
  end

  def self.apply(string_or_doc)
    doc = string_or_doc
    doc = Nokogiri::HTML::fragment(doc) if doc.is_a?(String)
    changed = false

    Oneboxer.each_onebox_link(doc) do |url, element|
      onebox, preview = yield(url,element)
      if onebox
        parsed_onebox = Nokogiri::HTML::fragment(onebox)
        next unless parsed_onebox.children.count > 0

        # special logic to strip empty p elements
        if  element.parent &&
            element.parent.node_name.downcase == "p" &&
            element.parent.children.count == 1 &&
            parsed_onebox.children.first.name.downcase == "div"
          element = element.parent
        end
        changed = true
        element.swap parsed_onebox.to_html
      end
    end

    Result.new(doc, changed)
  end

  def self.cache_key_for(url)
    "onebox:#{Digest::SHA1.hexdigest(url)}"
  end

  def self.preview_cache_key_for(url)
    "onebox:preview:#{Digest::SHA1.hexdigest(url)}"
  end

  def self.render_from_cache(url)
    Rails.cache.read(cache_key_for(url))
  end

  # Cache results from a onebox call
  def self.fetch_and_cache(url, args)
    contents, preview = onebox_nocache(url)
    return nil if contents.blank?

    Rails.cache.write(cache_key_for(url), contents, expires_in: default_expiry)
    if preview.present?
      Rails.cache.write(preview_cache_key_for(url), preview, expires_in: default_expiry)
    end

    [contents, preview]
  end

  def self.invalidate(url)
    Rails.cache.delete(cache_key_for(url))
  end

  def self.preview(url, args={})
    # Look for a preview
    cached = Rails.cache.read(preview_cache_key_for(url)) unless args[:no_cache].present?
    return cached if cached.present?

    # Try the full version
    cached = render_from_cache(url)
    return cached if cached.present?

    # If that fails, look it up
    contents, cached = fetch_and_cache(url, args)
    return cached if cached.present?
    contents
  end

  # Return the cooked content for a url, caching the result for performance
  def self.onebox(url, args={})

    if args[:invalidate_oneboxes]
      # Remove the onebox from the cache
      Oneboxer.invalidate(url)
    else
      contents = render_from_cache(url)
      return contents if contents.present?
    end

    fetch_and_cache(url, args)
  end

end
