#
# This file is part of a web load testing tool (currently having no name) 
# Copyright (C) 2008 Jittat Fakcharoenphol
#
# 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 2 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#

require 'rubygems'
require 'uri'

class Visitor

  attr_accessor :talkative

  class << self
    attr_accessor :commands
    attr_accessor :base_url
    attr_accessor :cookies_stored
  end

  def get_cookie_fname
    "#{@base_dir}/cookies.#{@id}"
  end

  def get_output_fname
    "#{@base_dir}/output.#{@id}"
  end

  def id
    @id
  end

  def initialize(id=0, base_dir='.')
    # initialize nil class variable
    self.class.base_url = "" if (self.class.base_url) == nil
    self.class.cookies_stored = false if self.class.cookies_stored == nil

    @id = id
    @base_dir = base_dir
    @cookies_fname = get_cookie_fname
    @output_fname = get_output_fname
    @statistics = Array.new
    @talkative = false

    @stopped = false
  end

  def cleanup
    trial = 0
    while FileTest.exists?(@cookies_fname)
      File.delete(@cookies_fname) 
      if FileTest.exists?(@cookies_fname)
        # wait until system returns
        puts "STILL HERE"
        sleep 1
        trial += 1
        break if trial>10
      end
    end 
    
    while FileTest.exists?(@output_fname)
      File.delete(@output_fname)
      if FileTest.exists?(@output_fname)
        # wait until system returns
        sleep 1
        trial += 1
        break if trial>10
      end
    end 
  end

  def self.site_url(url)
    self.base_url = url
  end

  def self.stores_cookies
    self.cookies_stored = true
  end

  def self.preprocess_param_hash(params)
    return {} if params==nil
    plist = {}
    params.each do |key,val|
      if key.is_a? Symbol
        key_s = key.to_s
      else
        key_s = key
      end
      plist[key_s] = val
    end
    plist
  end

  def self.get(url,params=nil)
    self.commands = [] if self.commands==nil
    self.commands << { 
      :command => :get, 
      :url => url, 
      :params => Visitor.preprocess_param_hash(params) }
  end

  def self.post(url,params=nil,options=nil)
    self.commands = [] if self.commands==nil
    self.commands << { :command => :post,
      :url => url, 
      :params => Visitor.preprocess_param_hash(params), 
      :options => options }
  end

  def self.process_options(options)
    if options.has_key? "site_url"
      self.base_url = options["site_url"]
    end
  end

  def substitute_id(st)
    return st if !(st.is_a? String)
    st.gsub(/(()|(\$))\$\{id\}/) do |s|
      if s=="${id}"
        @id.to_s
      else
        "${id}"
      end
    end
  end

  def encode_params(params)
    enc = ""
    if params!=nil and params.length!=0
      params.each do |key,val|
        if enc != ""
          enc += '&'
        end
        val = substitute_id(val)
        enc += URI.escape(key) + '=' + URI.escape(val.to_s)
      end
    end
    enc
  end

  def get(url,params)
    #build url

    #puts "----------------------cookies-----------"
    #system("cat #{@cookies_fname}")
    #puts "----------------------cookies-----------"

    full_url = "#{self.class.base_url}#{url}"
    if params!=nil and params.length!=0
      full_url += '?' + encode_params(params)
    end
    
    cmd = "curl -k -b #{@cookies_fname} -D #{@cookies_fname} #{full_url} " +
      " -s -L -o #{@output_fname}"
    #puts ">>>>>>>>>>>>>>>>>> " + cmd
    system(cmd)
    #system("cat #{@output_fname}")
  end

  def post(url,params,options)
    #puts "----------------------cookies-----------"
    #system("cat #{@cookies_fname}")
    #puts "----------------------cookies-----------"

    full_url = "#{self.class.base_url}#{url}"
    params_str = ""
    if options!=nil and options[:multipart]==true
      params.each do |key,val|
        if val.is_a? Hash
          case val[:type]
          when :file
            dval = substitute_id(val[:data])
            params_str += " -F \"#{key}=@#{dval.to_s}\""
          end
        else
          val = substitute_id(val)
          params_str += " -F \"#{key}=#{URI.escape(val.to_s)}\""
        end
      end
    else
      params_str += "-d \"#{encode_params(params)}\""
    end

    #puts params_str

    cmd = "curl -L -k -b #{@cookies_fname} -D #{@cookies_fname} " +
      " #{params_str} #{full_url} -s -o #{@output_fname}"
    #puts ">>>>>>>>>>>>>>>>>>>>>>>>>>> POST: " + cmd
    system(cmd)
    #system("cat #{@output_fname}")
  end

  def stop!
    @stopped = true
  end

  def run(times=nil, options={})
    times = 1 if times == :once

    @stopped = false
    while times!=0
      self.class.commands.each do |cmd|
        puts "#{@id}: #{cmd[:command]} #{cmd[:url]}" if @talkative

        start_time = Time.new

        if !options[:dry_run]
          case cmd[:command]
          when :get
            get cmd[:url], cmd[:params]
          when :post
            post cmd[:url], cmd[:params], cmd[:options]
          end
        end

        finish_time = Time.new

        break if @stopped

        @statistics << {
          :url => "#{cmd[:command]}:#{cmd[:url]}",
          :time => finish_time - start_time }
      end

      times -= 1 if times.is_a? Integer    #otherwise, run forever

      break if @stopped
    end
  end

  def show_raw_stat
    @statistics.each do |stat|
      puts "#{stat[:url]} => #{stat[:time]}"
    end
  end

  def statistics
    num_requested = @statistics.length
    totaltime = 0.0
    @statistics.each { |stat| totaltime += stat[:time] }

    if num_requested>0
      average_request_time = totaltime / num_requested
    else
      average_request_time = 0
    end

    sq_sum = 0.0
    @statistics.each do |stat| 
      sq_sum += (stat[:time]-average_request_time) ** 2
    end
    if num_requested>1
      sd = Math.sqrt(sq_sum/(num_requested-1))
    else
      sd = 0
    end

    return {
      :num_requested => num_requested,
      :avg_request_time => average_request_time,
      :std_dev => sd
    }
  end
end

def visitor(cname,options={},&blk)
  c = Class.new(Visitor)
  begin
    Object.const_set(cname,c)
  rescue NameError
    puts <<ERROR
Error on type #{cname}.
Type name should be capitalized and follow Ruby constant naming rule.
ERROR
    exit(0)
  end
  c.process_options(options)
  c.instance_eval(&blk)
end
