# This is a simple tool to help figure out when your goat will kid, though
# if can be easily adapted to other animals. The DatePicker class is the
# interesting bit, though, and may be useful elsewhere.
#
# Original author: Mike Gauland (mikelygee-at-gmail-dot-com)

# Configuration:
GESTATION_PERIOD = 150
GESTATION_MARGIN = 5

require 'date'

# DatePicker provides an interface for selecting a date from a monthly
# calendar. See the PickDate() method for details.
#
class DatePicker
  # These values can be used to size the window or slot for the DatePicker:
  DESIRED_WIDTH = 300
  DESIRED_HEIGHT = 240

  # Used to make the day columns uniform width:
  @@DAY_WIDTH = 1.0/7

  # PickDate() displays the calendar for a given month and year. Dates are
  # outlined as the move moves over them; when a date is clicked, it is passed
  # to the supplied block. The user can also move forward and backward one
  # month at a time.
  #
  def DatePicker.PickDate(slot, month, year, &block)
    # Determine the first and last days of the month. We use the first to
    # figure out the weekday on which the month starts; the last tells us
    # how many days are in the month.
    first = Date.civil(year, month, 1)
    last = Date.civil(year, month, -1)

    # 'app' represents the Shoes app; it is just a convenient shortcut.
    app = slot.app

    slot.clear() do
    slot.background(app.white)

      s = app.stack(:width => "100%") do
        # This stack contains the header. On top, we have the month name
        # and year, bracketed by links to the previous and next months.
        app.flow() {
          # Link to the previous month:
          app.stack(:width => 0.13) {
            app.para(app.link(Date::ABBR_MONTHNAMES[(first - 1).month],
                              :click => proc {|n|
                                DatePicker::PickDate(slot, 
                                                     (first - 1).month,
                                                     (first -1 ).year,
                                                     &block)
                              }))
          }
          # The current month and year: 
          app.stack(:width => 0.74) {
            app.tagline("#{Date::MONTHNAMES[month]} #{year}",
                         :align => "center")
          }

          # ...a the link to the next month
          app.stack(:width => 0.13, :right => 2) { 
            app.para(app.link( Date::ABBR_MONTHNAMES[(last + 1).month],
                                 :align => "right", :click => proc {|n|
                                   DatePicker::PickDate(slot,
                                                           (last + 1).month,
                                                           (last + 1 ).year,
                                                           &block)
                                 }))
          }
        }

        # Still in the header, we add the names of the days:
        app.flow do
          Date::ABBR_DAYNAMES.each {|day|
            app.stack(:width => @@DAY_WIDTH) do
                app.para(app.strong(day), :align => "right")
            end
          }
        end
      end

      # Now comes the tricky bit. We'll make the calendar six weeks long
      # (rather than trying to figure out if it will fit into fewer lines),
      # and so it doesn't change size as you scroll through the months).
      # Each cell will have a 'para', with either an empty string or a
      # number. If there is a number, it will include actions to outline
      # it when the mouse is on top of it, and to invoke the supplied block
      # when it is clicked.

      # This is the 'date' of the first cell. Unless the month starts
      # on the first day of the week, this will be less than 1. We'll ignore
      # 'day' values with are less than 1 or more than the number of days in
      # the month; this is just a mechanism to easily get numbers on the
      # right weekdays.
      day = -first.wday + 1

      # Again, we'll always show six weeks:
      6.times {
        # Each week is a 'flow' of seven days:
        f = app.flow(:width => "100%")
        s.append() { f }

        f.append() {
          7.times {
            # Each cell is a stack, containining a para with either an empty
            # string, or a day number. If the latter, the cell will outline
            # itself when the mouse is over it, and respond to mouse clicks.
            #
            app.stack(:width => @@DAY_WIDTH, :margin_left => 2) {
              if day > 0 and day <= last.day then
                p = app.para(day, :align => "right", :margin_bottom => 2)
                app.hover do |me| # Outline when the mouse is over
                  me.border(app.red)
                end
                
                app.leave do |me| # Remove the outline when the mouse leaves
                  me.border(app.white)
                end
                
                app.click do # Build a date object and invoke the block:
                  result = Date::civil(year, month, p.text.to_i)
                  yield(result)
                end
              end
              }
            day = day.next
          }
        }
      }
    end
  end
end

# Here's the main app. At the top of the window, we'll leave room for the
# calendar (DatePicker); below will be places for the breeding, due,
# early, and late dates.
#
Shoes.app(:width => 450, :height => 400, :title => "Kidding Calculator") do
  stack {
    flow {
      # This is space for the
      stack(:width => 75) {} # Space to center the calendar
      @dateSlot = stack(:width => DatePicker::DESIRED_WIDTH,
                        :height => DatePicker::DESIRED_HEIGHT,
                        :align => "center") {}
    }

    # Below the calendar are the dates.
    stack {
      stack {
        stack { # Breeding date
          para(strong("A doe bred on"), :align => "center")
          @bredDate = para("", :align => "center")
        }

        # Underneath, early, average, and late due dates:
        flow {
          stack(:width => 0.25) { # Early date
            para(strong("Early"), :align => "left")
            @earlyDate = para("", :align => "left")
          }
          
          stack(:width => 0.5) { # Average due date
            para(strong("Will kid on"), :align => "center")
            @dueDate = para("", :align => "center")
          }
          
          stack(:width => 0.25) { # Late date
            para(strong("Late"), :align => "right")
            @lateDate = para("", :align => "right")
          }
        }
      }
    }
  }

  # Define a block which sets the breeding date, and calculates the other
  # dates. This will be passed to the DatePicker.
  #
  on_date_change = proc {|date|
    @bredDate.replace(date.strftime("%e %B %Y"))
    due = date + GESTATION_PERIOD
    @earlyDate.replace((due + - GESTATION_MARGIN).strftime("%e %B"))
    @dueDate.replace((due).strftime("%e %B"))
    @lateDate.replace((due + GESTATION_MARGIN).strftime("%e %B"))
  }

  # Seed the breeding date with today:
  on_date_change.call(Date::today)

  # Put the DatePicker in the stack reserved for it:
  DatePicker::PickDate(@dateSlot, Date::today.month, Date::today.year,
                       &on_date_change)
end
