#---------------------------------------------------------------#
# incith:weather                                        v1.9_r7 #
#                                                               #
# fetches weather from wunderground.com.                        #
# tested on Eggdrop v1.6.17                                     #
#                                                               #
# Usage:                                                        #
#   .chanset #channel +weather                                  #
#   !weather, !forecast                                         #
#      zipcode; postal code; city; state/province;              #
#      city, country; airport/icao code                         #
#                                                               #
# ChangeLog:                                                    #
#   1.1: first public release                                   #
#   1.2: removed local time from weather{} and replaced it with #
#          lat/lon & elevation instead                          #
#        updated the regexps for local time & the last updated  #
#          times to strip off the date                          #
#   1.3: work on grabbing a result from list of results         #
#   1.4: updates to forecast                                    #
#        flood protection                                       #
#        wrapping of long output lines                          #
#        private message support (for /msg !weather)            #
#        not grabbing pressure or visibility to save space      #
#        optional bolding of headers                            #
#   1.5: changed putserv{}'s to putquick{}'s                    #
#        fix for how it grabs wind.  sorry, unforseen.          #
#        fix to display high/low in hopefully all cities now    #
#   1.6: argh, forgot to pass windchill into my new format proc #
#   1.7: bugfix for no such variable "updated" bug              #
#   1.8: fixed '!w !fc !w !fc' bug, case insensitive binds too  #
#    .1: forgot to update flood protection, it's now proper     #
#    .2: wunderground has updated their website                 #
#    .3: fix for the slow !weather results                      #
#    .4: windchill re-added                                     #
#    .5: Can use 'special' chars as command_char again (§)      #
#    .6: ...sigh, fix for forecast.                             #
#   1.9: added conditions to forecast                           #
#        fix for american locations                             #
#    r2: fix for american locations again.  sorry it took ages. #
#    r3: bleh.                                                  #
#    r4: lots of fixes (humidity, wind, temperature, etc..)     #
#    r5: will grab the first result found on multiple results   #
#    r6: made $seperator work; seperate_lines setting added     #
#    r7: forgot to put the r5 fix into forecast lookups, fixed  #
#                                                               #
# BUGS:                                                         #
#   - If you find something quirky, my e-mail is at the bottom  #
#     of this header.                                           #
#                                                               #
# TODO:                                                         #
#   - store default weather settings per user                   #
#   - different ways to format the output                       #
#   + do something with multiple results                        #
#                                                               #
# LICENSE:                                                      #
#   This code comes with ABSOLUTELY NO WARRANTY.                #
#                                                               #
#   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. (http://www.gnu.org/copyleft/library.txt)          #
#                                                               #
# Copyright (C) 2005, Jordan                                    #
# incith@gmail.com                                              #
#---------------------------------------------------------------#
package require http 2.3

namespace eval incith {
  namespace eval weather {
    # the character needed to be prefix'd to a bind (ie !weather, "." would be .weather)
    variable command_char "!"

    # the binds themselves, "one two three"
    variable weather_binds "w weather"
    variable forecast_binds "fc forecast"

    # allow private message lookups? 0 to disable
    variable private_messages 1

    # what to use to seperate results (use "\n" to put everything into a seperate line)
    variable seperator "; "

    # put results into three seperate lines if the seperator is not "\n"
    variable seperate_lines 0

    # this character will be displayed immediately after a temperature
    variable degree_symbol "°"

    # ** this is not an optional setting, if a string is too long to send, it won't be sent! **
    # It should be set to the max amount of characters that will be received in a public
    #   message by your IRC server.  If you find you aren't receiving results, try lowering this.
    variable split_length 443

    # replace headers appearing in the description as bolded words?
    variable bold_descriptions 1

    # number of minute(s) to ignore flooders, 0 to disable flood protection
    variable ignore 1

    # how many requests in how many seconds is considered flooding?
    # by default, this allows 3 queries in 10 seconds, the 4th being ignored
    #   and ignoring requests for 'variable ignore' minutes
    variable flood 4:10
  }
}

# script begings
namespace eval incith {
  namespace eval weather {
    variable version "incith:weather-1.9_r7"
  }
}

# bind the binds
foreach bind [split $incith::weather::weather_binds " "] {
  bind pub -|- "${incith::weather::command_char}${bind}" incith::weather::weather_message
  if {$incith::weather::private_messages >= 1} {
    bind msg -|- "${incith::weather::command_char}${bind}" incith::weather::weather_message
  }
}

foreach bind [split $incith::weather::forecast_binds " "] {
  bind pub -|- "${incith::weather::command_char}${bind}" incith::weather::forecast_message
  if {$incith::weather::private_messages >= 1} {
    bind msg -|- "${incith::weather::command_char}${bind}" incith::weather::forecast_message
  }
}

namespace eval incith {
  namespace eval weather {
    # [weather] : fetches the current weather from wunderground.com.
    #
    proc weather {input} {
      # fetch the html
      set html [fetch_html $input]

      # check for htmls existence
      if {[info exists html] == 0} {
        return "There was a problem while connecting to wunderground.com."
      }

      # check for multiple results
      if {[regexp {<h1>Search Results:</h1>} $html]} {
        regexp {<a href="/global/stations/(\d+?)\.html">} $html - first_found
        if {[info exists first_found]} {
          set html [fetch_html $first_found 1]
        }
      }

      # location
      regexp {padding-bottom: 3px;(?: width: 100%;)">  <h1>(.+?)\s*</h1>  </td>} $html - location
      if {[info exists location] == 0} {
        regexp {<div id="smB">  (.+?)  </div>} $html - location
        if {[info exists location]} {
          regsub -all {\s{1,}} $location { } location
        }
      }
      if {[info exists location] == 0} {
        return "I could not find '$input'."
      }

      # time weather was last updated
      regexp {Updated:</span> (.+?)(?:</h4>|</b>)} $html - updated
      if {[info exists updated]} {
        regexp {(.+?) on (.+?)} $updated - updated updated_date
        regsub -all "\<.*?\>" $updated "" updated
      }

      # latitude/longitude
      regexp {nowrap;"><b>(.+?)&#176; (.+?) (.+?)&#176; (.+?)</b>} $html - lat lath lon lonh

      # elevation
      regexp {Elevation:.*?<nobr><b>(.+?)</b>&nbsp;ft</nobr>  /  <nobr><b>(.+?)</b>&nbsp;m</nobr>} $html - elevf elevm

      # temperature
      regexp {pwsvariable="tempf" english="&deg;F" metric="&deg;C" value="(.+?)">  <nobr><b>.+?</b>&nbsp;&#176;F</nobr>  /  <nobr><b>(.+?)</b>&nbsp;&#176;C</nobr></span></div>} $html - tempf tempc
      # regexp {<nobr><b>(.+?)</b>&nbsp;&#176;F</nobr>  /  <nobr><b>(.+?)</b>&nbsp;&#176;C</nobr>} $html - tempf tempc
      # regexp {white-space: nowrap;">  <nobr><b>(.+?)</b>&nbsp;&#176;F</nobr>.*?<nobr><b>(.+?)</b>.*?&nbsp;&#176;C</nobr></div>} $html - tempf tempc
      if {![info exists tempf]} { 
        regexp {Temperature:.*?">  <nobr><b>(.+?)</b>&nbsp;&#176;F</nobr>  /  <nobr><b>(.+?)</b>&nbsp;&#176;C</nobr>} $html - tempf tempc
      }
      if {![info exists tempf]} { 
        regexp {<td class="full" id="message2">  <div style="font-size: 17px;">  <nobr><b>(.+?)</b>&nbsp;&#176;F</nobr>  /  <nobr><b>(.+?)</b>&nbsp;&#176;C</nobr>} $html - tempf tempc
      }
      if {![info exists tempf]} { 
        return "$location; Could not fetch the current temperature."
      }

      # highs & lows for today
      set regexp_highlow {<span style="color: #900;">  (.+?)  &deg;  </span>  \|  <span style="color: #009;">  (.+?)  .*?</span>}
      regexp $regexp_highlow $html - highf lowf

      # current conditions
      regexp {<div id="b" style="font-size: 14px;">(.+?)</div>} $html - cond

      # humidity
      regexp {pwsvariable="humidity".*?><nobr><b>(.+?)</b>} $html - hum
      # regexp {Humidity:.*?">(.+?)</td>  </tr>} $html - hum

      # dew point
      regexp {Dew Point:.*?">  <nobr><b>(.+?)</b>&nbsp;&deg;F</nobr>  /  <nobr><b>(.+?)</b>&nbsp;&deg;C</nobr></span></td>} $html - dewf dewc

      # uv index
      regexp {UV:.*?">(.+?) <span.*">out of (.+?)</span></td>} $html - uvcur uvmax

      # wind mph, wind kph
      regexp {Wind:.*?">  <span class=.*?pwsvariable="windspeedmph".*?>  <nobr><b>(.+?)</b>&nbsp;mph</nobr>  /  <nobr><b>(.+?)</b>&nbsp;km/h</nobr>  </span>  <span style="font-weight: normal;">&nbsp;from the </span>.*?pwsvariable="winddir".*?value="(.+?)">} $html - windm windk windd
      #regexp {Wind:.*?">  <nobr><b>(.+?)</b>&nbsp;mph</nobr>  /  <nobr><b>(.+?)</b>&nbsp;km/h</nobr>  <span style="font-weight: normal;"> from the </span>  (.+?)  </td>  </tr>} $html - windm windk windd

      # windchill
      regexp {Windchill:.*?">  <nobr><b>(.+?)</b>&nbsp;&#176;F</nobr>  /  <nobr><b>(.+?)</b>&nbsp;&#176;C</nobr>} $html - chillf chillc

      # end parsing, make a reply
      set output "[ih $location]"
      if {$incith::weather::seperate_lines >= 1} {
        set second_sep "\n"
      } else {
        set second_sep $incith::weather::seperator
      }

      if {[info exists lat] && [info exists lon]} {
        if {[info exists elevm]} {
          append output " (${lat}${incith::weather::degree_symbol}${lath}/${lon}${incith::weather::degree_symbol}${lonh}, ${elevm}M)"
        } else {
          append output " (${lat}${incith::weather::degree_symbol}${lath}/${lon}${incith::weather::degree_symbol}${lonh})"
        }
      }

      if {[info exists updated]} {
        append output "${incith::weather::seperator}[ih "Last Updated:"] $updated"
      }
      if {[info exists dewf]} {
       append output "${second_sep}[ihfc "Dew Point:" $dewf $dewc]"
      }
      if {[info exists hum]} {
        append output "${incith::weather::seperator}[ih "Humidity:"] $hum"
      }
      append output "${incith::weather::seperator}[ihfc "Temperature:" $tempf $tempc]"
      if {[info exists highf] && [info exists lowf]} {
        append output "${incith::weather::seperator}[ih "High/Low:"] [if2c $highf $lowf]"
      }
      if {[info exists cond]} {
        append output "${second_sep}[ih "Conditions:"] $cond"
      }
      if {[info exists uvcur] && [info exists uvmax]} {
        append output "${incith::weather::seperator}[ih "UV Index:"] $uvcur/$uvmax"
      }
      if {[info exists windm] && [info exists windk]} {
        if {$windm != 0 && $windk != 0} {
          append output "${incith::weather::seperator}[ih "Wind:"] $windm MPH ($windk KPH)"
          if {[info exists windd]} {
            if {[llength $windd] > 0} {
              append output " from the $windd"
            }
          }
        }
      } else {
        append output "${incith::weather::seperator}[ih "Wind:"] Calm"
      }
      if {[info exists chillf]} {
        append output "${incith::weather::seperator}[ihfc "Windchill:" $chillf $chillc]"
      }

      # return the reply
      if {[info exists output] == 0} {
        return "Something went wrong while trying to parse the weather."
      } else {
        return $output
      }
    }

    # [forecast] : fetches the 5-day forecast from wunderground.com.
    #
    proc forecast {input} {
      # fetch the html
      set html [fetch_html $input]

      # check for htmls existence
      if {[info exists html] == 0} {
        return "There was a problem while connecting to wunderground.com."
      }

      # check for multiple results
      if {[regexp {<h1>Search Results:</h1>} $html]} {
        regexp {<a href="/global/stations/(\d+?)\.html">} $html - first_found
        if {[info exists first_found]} {
          set html [fetch_html $first_found 1]
        }
      }

      # location
      regexp {padding-bottom: 3px;(?: width: 100%;)">  <h1>(.+?)\s*</h1>  </td>} $html - location
      if {[info exists location] == 0} {
        regexp {<div id="smB">  (.+?)  </div>} $html - location
        if {[info exists location]} {
          regsub -all {\s{1,}} $location { } location
        }
      }
      if {[info exists location] == 0} {
        return "I could not find '$input'."
      }

      # these are the 5-day forecast days
      set regexp_day {<td style="width: 20%;">  <h4>(.+?)</h4>.*?</td>\s*}
      regexp -- "${regexp_day}${regexp_day}${regexp_day}${regexp_day}${regexp_day}" $html - day1 day2 day3 day4 day5

      # these are the respective highs & lows for those days
      set regexp_highlow {<span style="color: #900;">  (.+?)  &deg;  </span>  \|  <span style="color: #009;">  (.+?)  &deg;  </span>}
      regexp -- "${regexp_highlow}.*${regexp_highlow}.*${regexp_highlow}.*${regexp_highlow}.*${regexp_highlow}" $html - day1h day1l day2h day2l day3h day3l day4h day4l day5h day5l

      # conditions for those days
      set regexp_cond {<td style="border-left: 1px solid #DDD; border-right: 1px solid #DDD; padding: 2px;">  (.*?)  (?:</td>|<div>)}
      regexp -- "${regexp_cond}.*${regexp_cond}.*${regexp_cond}.*${regexp_cond}.*${regexp_cond}" $html - day1c day2c day3c day4c day5c

      # bring it all together
      set day1w "[ih "${day1}:"] ${day1c}, [if2c $day1h $day1l]"
      set day2w "[ih "${day2}:"] ${day2c}, [if2c $day2h $day2l]"
      set day3w "[ih "${day3}:"] ${day3c}, [if2c $day3h $day3l]"
      set day4w "[ih "${day4}:"] ${day4c}, [if2c $day4h $day4l]"
      set day5w "[ih "${day5}:"] ${day5c}, [if2c $day5h $day5l]"

      # make a reply
      set output "[ih "${location} Forecast (High/Low)"]${incith::weather::seperator}"
      if {[info exists day1h] && [info exists day2h] && [info exists day3h] && [info exists day4h] && [info exists day5h]} {
        append output "${day1w}${incith::weather::seperator}${day2w}${incith::weather::seperator}"
        append output "${day3w}${incith::weather::seperator}${day4w}${incith::weather::seperator}${day5w}"
      } else {
        append output "The 5-Day Forecast was unavailable."
      }

      return $output
    }

    # FETCH_HTML
    # fetches the html from wunderground.com and returns html
    #
    proc fetch_html {input args} {
      global tcl_platform

      # setup & search for the weather on wunderground
      set ua "TCL/[info tclversion] ([string totitle $tcl_platform(platform)]; $tcl_platform(os)/$tcl_platform(osVersion)) http/[package provide http]"

      if {[llength $args] < 1} {
        set query "http://www.wunderground.com/cgi-bin/findweather/getForecast?query="
        set input [::http::formatQuery $input]
        set input [string trimleft $input "+"]
        set query "${query}${input}"
      } else {
        set query "http://www.wunderground.com/global/stations/${input}.html"
      }
      set http [::http::config -useragent $ua]
      set http [::http::geturl $query]
      set html [::http::data $http]
      # clean up our response
      regsub -all {(?:\n|\t)} $html {} html

      if {[info exists html]} {
        return $html
      } else {
        return "Something failed while attempting to connect to wunderground.com."
      }
    }

    # WEATHER_MESSAGE
    # handles public/private messages for [weather] requests
    #
    proc weather_message {nick uhand hand chan {input 0}} {
      proc send_weather {nick uhand where input} {
        # flood protection
        if {[flood $nick $uhand]} {
          return 0
        }
        # call weather
        foreach line [incith::weather::parse_output [weather $input]] {
          putquick "PRIVMSG $where :$line"
        }
      }
      if {$input == 0} {
        send_weather $nick $uhand $nick $chan
      } elseif {[lsearch -exact [channel info $chan] +weather] != -1} {
        send_weather $nick $uhand $chan $input
      }
    }

    # FORECAST_MESSAGE
    # handles public/private messages for [forecast] requests
    #
    proc forecast_message {nick uhand hand chan {input 0}} {
      proc send_forecast {nick uhand where input} {
        # flood protection
        if {[flood $nick $uhand]} {
          return 0
        }
        # call forecast
        foreach line [incith::weather::parse_output [forecast $input]] {
          putquick "PRIVMSG $where :$line"
        }
      }
      if {$input == 0} {
        send_forecast $nick $uhand $nick $chan
      } elseif {[lsearch -exact [channel info $chan] +weather] != -1} {
        send_forecast $nick $uhand $chan $input
      }
    }

    # IF2C
    # converts header + fahrenheit to celcius & returns a formatted string
    #
    proc if2c {fh fl} {
      set ch [expr {int((5/9.)*((${fh} + 0.5) - 32))}]
      set cl [expr {int((5/9.)*((${fl} + 0.5) - 32))}]
      return "${fh}/${fl}${incith::weather::degree_symbol}F (${ch}/${cl}${incith::weather::degree_symbol}C)"
    }

    # IH
    # formats a header
    #
    proc ih {h} {
      if {$incith::weather::bold_descriptions >= 1} {
        regsub -all -nocase {(.*)} $h "\002&\002" h
      }
      return $h
    }

    # IHFC
    # formats a header + two temperatures
    #
    proc ihfc {h f c} {
      return "[ih $h] ${f}${incith::weather::degree_symbol}F (${c}${incith::weather::degree_symbol}C)"
    }

    # PARSE_OUTPUT
    # prepares output for sending to a channel/user, calls line_wrap
    #
    # PARSE_OUTPUT
    # prepares output for sending to a channel/user, calls line_wrap
    #
    proc parse_output {input} {
      set parsed_output [set parsed_current {}]
      if {[string match "\n" $incith::weather::seperator] == 1 || $incith::weather::seperate_lines >= 1} {
        regsub {\n\s*$} $input "" input
        foreach newline [split $input "\n"] {
          foreach line [incith::weather::line_wrap $newline] {
            lappend parsed_output $line
          }
        }
      } else {
        regsub "(?:${incith::weather::seperator}|\\|)\\s*$" $input {} input
        foreach line [incith::weather::line_wrap $input] {
          lappend parsed_output $line
        }
      }
      return $parsed_output
    }

    # LINE_WRAP
    # takes a long line in, and chops it before the specified length
    # http://forum.egghelp.org/viewtopic.php?t=6690
    #
    proc line_wrap {str {splitChr { }}} {
      set out [set cur {}]
      set i 0
      set len $incith::weather::split_length
      foreach word [split [set str][set str ""] $splitChr] {
        if {[incr i [string len $word]] > $len} {
          lappend out [join $cur $splitChr]
          set cur [list $word]
          set i [string len $word]
        } else {
          lappend cur $word
        }
        incr i
      }
      lappend out [join $cur $splitChr]
    }

    # FLOOD_INIT
    # modified from bseen
    #
    variable flood_data
    variable flood_array
    proc flood_init {} {
      if {$incith::weather::ignore < 1} {
        return 0
      }
      if {![string match *:* $incith::weather::flood]} {
        putlog "$incith::weather::version: variable flood not set correctly."
        return 1
      }
      set incith::weather::flood_data(flood_num) [lindex [split $incith::weather::flood :] 0]
      set incith::weather::flood_data(flood_time) [lindex [split $incith::weather::flood :] 1]
      set i [expr $incith::weather::flood_data(flood_num) - 1]
      while {$i >= 0} {
        set incith::weather::flood_array($i) 0
        incr i -1
      }
    }
    ; flood_init

    # FLOOD
    # updates and returns a users flood status
    #
    proc flood {nick uhand} {
      if {$incith::weather::ignore < 1} {
        return 0
      }
      if {$incith::weather::flood_data(flood_num) == 0} {
        return 0
      }
      set i [expr ${incith::weather::flood_data(flood_num)} - 1]
      while {$i >= 1} {
        set incith::weather::flood_array($i) $incith::weather::flood_array([expr $i - 1])
        incr i -1
      }
      set incith::weather::flood_array(0) [unixtime]
      if {[expr [unixtime] - $incith::weather::flood_array([expr ${incith::weather::flood_data(flood_num)} - 1])] <= ${incith::weather::flood_data(flood_time)}} {
        putlog "$incith::weather::version: flood detected from ${nick}."
        newignore [join [maskhost *!*[string trimleft $uhand ~]]] $incith::weather::version flooding $incith::weather::ignore
        return 1
      } else {
        return 0
      }
    }
  }
}

putlog " - $incith::weather::version loaded."

# EOF
