#!/usr/bin/ruby

require 'cgi'
require 'net/http'
require 'yaml'
require 'rubygems'
require 'builder'

require 'builder_monkey_patch'

MDASH = [0x2014].pack('U')
STAR  = [0x2605].pack('U')
TITLE = 'Ruby Family Bookmarks'
DAYS  = 7

JID, PASSWORD = "rubys@rubix/bookmark", "password"
SUBSCRIPTIONS = %w(
  rubys@rubix/Laptop
  rubyc@rubix/Laptop
)

cgi = CGI.new
x = Builder::XmlMarkup.new :indent => 2

cgi.out 'type' => 'application/xhtml+xml', 'charset' => 'UTF-8' do
  x.html :xmlns => 'http://www.w3.org/1999/xhtml' do
    x.header do
      x.link :rel => 'shortcut icon', :href => 'star.png'
      x.title TITLE

      x.text! "\n"
      x.style :type => "text/css" do
        x.text! <<-EOF.gsub(/^    /,'')
          html {margin: 0}
          h1 {margin: -8px -10px 0 -10px; padding: 0.4em}
          h1 {background: url("/images/blueback.png") repeat-x}
 
          form, input, textarea, button, pre {-moz-border-radius: 1em}
          form, pre {border: solid; margin: 1em; padding: 2em}
          input[type="text"], textarea {width: 100%; padding: 2px}
          input[type="checkbox"] {margin-left: 1em}
          textarea {margin-bottom: 1em}
          table {width: 100%}
 
          ul {list-style: none}
          .icon { height: 16px; width: 16px; margin-right: 1px }
 
          h1 {background-color: #039}
          h1 a {color: #FF0; text-decoration: none}
          form {background-color: #FE0}
          pre {background-color: #FCC}
          input, textarea {background-color: #FF8}
          button {margin: 0 1em 0 2em; background-color: #FFC}
          li > span:first-child { color: #039}
        EOF
      end

      x.text! "\n"
      x.script :type => "text/javascript" do
        x.text! <<-'EOF'.gsub(/^    /,'')
          function text_state(state) {
            var inputs = document.getElementsByTagName('input');
            for (var i=0; i<inputs.length; i++) {
              if (inputs[i].type == 'text') inputs[i].disabled = state;
            }
            comment_focus();
          }

          function comment_focus() {
            var comments = document.getElementById("comments");
            comments.value = comments.value.replace(/^\s*/, '');
            comments.focus();
          }
        EOF
      end
    end

    x.text! "\n"

    x.body :onload => 'comment_focus()' do
      begin
        x.h1 { x.a "#{STAR} #{TITLE}", :href => '.' }
        if !cgi.params.empty?
          if cgi.request_method == 'GET'

            # input form
            x.text! "\n"
            x.form :method => 'post', :action => '.' do
              x.table do

                # input fields
                cgi.params.each do |name, value|
                  next if name == 'user'
                  x.tr do
                    x.td name.capitalize
                    x.td do
                      x.input :type=>'text', :size => 80,
                        :name =>name, :value => value.join
                    end
                  end
                end

                # comment text area
                x.tr do
                  x.td 'Comments'
                  x.td do
                   x.textarea(:name => 'comments', :id => 'comments') {}
                  end
                end

                # hidden field, buttons, and checkbox
                x.tr do
                  x.td do
                   x.input :type=>'hidden',
                     :name =>'user', :value => cgi['user']
                  end
                  x.td do
                    x.button "post and alert", :type => "submit",
                      :name => "post", :value =>"alert"
                    x.button "post only", :type => "submit",
                      :name => "post", :value =>"only"
                    x.input 'text only', :type => 'checkbox',
                      :name => 'text', :id => 'text', :value => 'only',
                      :onclick => 'text_state(this.checked)'
                    x.label 'text only', :for => 'text'
                  end
                end
              end
            end

          else 

            # defaults
            favicon = nil
            name = Time.now.iso8601

            if cgi.has_key? 'uri'
              uri = URI.parse(cgi.params['uri'].first)

              # default location for favicon
              favicon = URI.parse("#{uri.scheme}://#{uri.host}/favicon.ico")

              # look for an explicit favicon link
              begin
                Net::HTTP.start(uri.host, uri.port) {|http|
                  response=http.get(uri.request_uri)
                  require 'html5'
                  doc = HTML5.parse(response.body)
                  doc.elements.each('html/head/link[@rel]') do |link|
                    # http://germane-software.com/projects/rexml/ticket/131
                    next unless link.attributes['href']
                    next unless link.attributes['rel'].split.any? {|rel|
                      %w(shortcut icon).include? rel
                    }
                    favicon = URI.join(uri.to_s, link.attributes['href'])
                  end
                }
              rescue
              end

              # verify that the favicon exists
              begin
                Net::HTTP.start(favicon.host, favicon.port) {|http|
                  favicon = nil if http.get(favicon.request_uri).code != '200'
                }
              rescue
                favicon = nil
              end

              # base the data file name on the uri
              require 'addressable/uri'
              name = Addressable::URI.parse(uri.to_s).normalize.to_s
            end

            # determine data file name for bookmark
            name.sub!(/^\w+:\/*(\w+:|www\.)?/,'') # remove scheme and www.
            name.gsub! /[?\/:|]+/, ','            # replace separator characters
            name.sub! /^[,.]*/, ''                # remove initial junk
            name.sub! /[,.]*$/, ''                # remove final junk
  
            # limit name length
            if name.length > 250
              require 'digest/md5'
              parts, excess = name.split(','), []
              excess << parts.pop while parts.join(',').length > 220
              parts << Digest::MD5.hexdigest(excess.join(','))
              name = parts.join(',')
            end
  
            # write out data file
            File.open(File.join('data',name),'w') do |file|
              params = {'favicon' => favicon.to_s}
              cgi.params.each {|n,v| params[n] = v.join(' ')}
              file.write(params.to_yaml)
            end

            # notify
            if cgi['post'] != 'only'
              require 'xmpp4r/client'
              client = Jabber::Client.new(Jabber::JID.new(JID))
              client.connect.auth(PASSWORD)

              html = REXML::Element.new('html')
              html.add_namespace('http://jabber.org/protocol/xhtml-im')
              body = html.add_element('body')
              body.add_namespace('http://www.w3.org/1999/xhtml')

              if cgi.has_key? 'uri'
                link = body.add_element('a')
                # http://germane-software.com/projects/rexml/ticket/127
                link.attributes['href'] = cgi['uri']
                link.text = cgi['title']
              end

              if !cgi['comments'].strip.empty?
                body.add_text " #{MDASH} " if cgi.has_key? 'uri'
                body.add_text cgi['comments']
              end

              text = cgi['uri'] + body.texts.map {|text| text.value}.join

              SUBSCRIPTIONS.each do |user|
                next if user == cgi['user'] or user =~ /^#{cgi['user']}@/

                msg = Jabber::Message.new(user, text)
                msg.add_element html
                client.send(msg)
              end

              client.close
            end
          end
        end

        # group bookmarks by date
        by_date = Hash.new {|hash,key| hash[key] = Array.new}
        Dir['data/*'].each do |file|
          next if file == 'index.cgi'
          mtime = File.mtime(file)
          date = Date.new(mtime.year, mtime.month, mtime.day)
          by_date[date] << [mtime, file]
        end

        # output last seven days of bookmarks
        by_date.keys.sort.reverse[0...DAYS].each do |date|
          x.text! "\n"
          x.h3 date.strftime("%A, %B %d, %Y").gsub(' 0',' ')
          by_date[date].sort.reverse.each do |mtime, file|
            bookmark = YAML.load(File.open(file))
            x.ul do
              x.li :class => bookmark['user'] do
                if bookmark['favicon'] and !bookmark['favicon'].empty?
                  x.img :src => bookmark['favicon'], :class => 'icon'
                else
                  x.span "#{STAR} "
                end
                if bookmark.has_key? 'uri'
                  x.a bookmark['title'], :href=>bookmark['uri']
                end
                if !bookmark['comments'].strip.empty?
                  x.text! " #{MDASH} " if bookmark.has_key? 'uri'
                  x.text! "#{bookmark['comments']}\n"
                end
              end
            end
          end
        end

      rescue Exception => e
        # if things go wrong, let somebody know
        x.pre do
          x.text! "#{e.inspect}\n"
          e.backtrace.each {|line| x.text! "  #{line}\n"}
        end
      end
    end
  end
end
    
