/*----------------------------------------------------------------------------*/
/*                                                                            */
/* Copyright (c) 2011-2014 Rexx Language Association. All rights reserved.    */
/*                                                                            */
/* This program and the accompanying materials are made available under       */
/* the terms of the Common Public License v1.0 which accompanies this         */
/* distribution. A copy is also available at the following address:           */
/* https://www.oorexx.org/license.html                                        */
/*                                                                            */
/* Redistribution and use in source and binary forms, with or                 */
/* without modification, are permitted provided that the following            */
/* conditions are met:                                                        */
/*                                                                            */
/* Redistributions of source code must retain the above copyright             */
/* notice, this list of conditions and the following disclaimer.              */
/* Redistributions in binary form must reproduce the above copyright          */
/* notice, this list of conditions and the following disclaimer in            */
/* the documentation and/or other materials provided with the distribution.   */
/*                                                                            */
/* Neither the name of Rexx Language Association nor the names                */
/* of its contributors may be used to endorse or promote products             */
/* derived from this software without specific prior written permission.      */
/*                                                                            */
/* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS        */
/* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT          */
/* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS          */
/* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT   */
/* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,      */
/* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED   */
/* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,        */
/* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY     */
/* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING    */
/* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS         */
/* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.               */
/*                                                                            */
/*----------------------------------------------------------------------------*/


/**
 * A more advanced example of the date and time picker control.
 *
 * This is example is intended to show how to use call back fields in the format
 * string for the DTP control and how to respond to the FORMAT, FORMATQUERY, and
 * KEYDOWN notifications for the call back fields.
 *
 * What is not explicit in the comments in this program is that there must be
 * a custom format string, containing call back fields, set in the DTP control
 * in order for the 3 event notifications to occur.
 *
 * In the program, change the date and time in the DTP control to see how the
 * call back fields work.  Select a call back field by clicking on it or tabbing
 * to it and then use the up or down arrow keys, home, end, page up, or page
 * down keys and observe what happens.
 */

  sd = locate()
  .application~setDefaults('O', sd'fiscalReports.h', .false)

  dlg = .FiscalReports~new(sd'fiscalReports.rc', IDD_FISCAL_REPORTS)
  dlg~execute("SHOWTOP")

return 0

::requires "ooDialog.cls"

::class 'FiscalReports' subclass RcDialog

::method init
  expose haveSizes

  forward class (super) continue

  self~setUpArrays
  haveSizes = .false

  self~connectDateTimePickerEvent(IDC_DTP_REPORT, "FORMAT", onFormat)
  self~connectDateTimePickerEvent(IDC_DTP_REPORT, "FORMATQUERY", onFormatQuery)
  self~connectDateTimePickerEvent(IDC_DTP_REPORT, "KEYDOWN", onKeyDown)

  do i = .constDir[IDC_RB_PRE_FIRST] + 1 to .constDir[IDC_RB_ANTE_LAST] - 1
    self~connectButtonEvent(i, 'CLICKED', onRbClick, .true)
  end

  self~connectButtonEvent(IDC_PB_PRINT, 'CLICKED', onPrint)

::method initDialog
  expose dtp currentType

  dtp = self~newDateTimePicker(IDC_DTP_REPORT);
  self~newRadioButton(IDC_RB_BAL_SHEET)~check

  -- Set up the range of the DTP control.  We say we can't print a report in the
  -- future, so the maximum date is set to the current time.  We say the company
  -- first opened its doors on 3/2/1998 (which happens to be a Monday) at 8 AM,
  -- so a report can not start prior to that date.
  now = .DateTime~new
  start = .DateTime~fromIsoDate('1998-03-02T08:00:00.000000')
  dtp~setRange(.Array~of(start, now))

  -- This is our format string with 3 call back fields in it.  Each call back
  -- field is designated by using a capital X. The number of X's allow you to
  -- uniquely indentify different call back fields.
  --
  -- As you see the format string starts with 3 call back fields.  The first is
  -- the number field, (First, Second, etc.,) the second is the period field,
  -- (quarter, half, etc.,) and the third is the report type, (balance, cash
  -- flow, etc..)  Within the format string, constant strings like:
  --   report starts from:
  -- need to be put in single quotes.  Punctuation and spaces do not need to be
  -- quoted, so for example ', ' does not need the single quotes.
  dtp~setFormat("XX XXX XXXX 'report starts from: 'hh':'mm tt dddd MMMM dd', 'yyyy")


/** onFormatQuery()
 *
 * This is the format query notification event handler. The DTP control sends
 * the notification to request the size of the largest item to be displayed in
 * the specified call back field
 *
 * @param  field  The identifier of the call back field.  In our case it will be
 *                XX, or XXX, or XXXX.
 *
 * @param  size   A .Size object.  The size of the largest item to display is
 *                returned to the DTP by setting this .Size object.
 *
 * The id and hwnd arguments are not needed here.
 *
 * Since the largest size for each field is not going to change, we just
 * calculate the size for each field 1 time and save a reference to it.  To
 * accurately calculate the size in pixels, we need to wait until the underlying
 * dialog is actually created.  We could have done this in initDialog(), but
 * I think it is better to wait until the DTP asks for the size.
 */
::method onFormatQuery unguarded
  expose dtp haveSizes xxSize xxxSize xxxxSize
  use arg field, size, id, hwnd

  if \ haveSizes then do
    xxSize  = self~calcSize('XX')
    xxxSize = self~calcSize('XXX')
    xxxxSize = self~calcSize('XXXX')
    haveSizes = .true
  end

  -- The equateTo() method sets the cx and cy attributes of the receiver .Size
  -- object to the cx and cy attributes of the argument .Size object.
  select
    when field == 'XX' then size~equateTo(xxSize)
    when field == 'XXX' then size~equateTo(xxxSize)
    otherwise size~equateTo(xxxxSize)
  end
  -- End select

  return 0

/** onFormat()
 *
 * This is the format notification event handler.  The DTP control sends this
 * notification to request the text to display in the call back field at this
 * point in time.  For this application, we base what should be displayed in the
 * numbers field on the currently selected date and time.  What the other 2
 * fields should display is tracked through the currentType and currentPeriod
 * variables.
 *
 * @param  field  The identifier of the call back field.  In our case it will be
 *                XX, or XXX, or XXXX.
 *
 * @param  dt     A .DateTime object.  This is the currently selected date and
 *                time in the DTP control.
 *
 * The id and hwnd arguments are not needed here.
 *
 * The return from this method is what the DTP control will dislpay in the
 * specified call back field.
 */
::method onFormat unguarded
  expose periods types currentType currentPeriod
  use arg field, dt, id, hwnd

  select
    when field == 'XX' then do
      ret = self~getPeriodNumber(dt)
    end

    when field == 'XXX' then do
      ret = periods[currentPeriod]
    end

    otherwise do
      ret = types[currentType]
    end
  end
  -- End select

  return ret

/** onKeyDown()
 *
 * This is the event handler for the KEYDOWN notification event handler.  The
 * DTP control sends this notification when the user types a key in a call back
 * field.  This allows the programmer to implement some custom behavior for the
 * call back fields.
 *
 * @param  field  The identifier of the call back field.  In our case it will be
 *                XX, or XXX, or XXXX.
 *
 * @param  dt     A .DateTime object.  This is the currently selected date and
 *                time in the DTP control.
 *
 * @param vKey    This is the virtual key the user pressed.
 *
 * The id and hwnd arguments are not needed here.
 *
 * The return is the modified date and time information based on the user's
 * keystroke.
 *
 * Each time the date and time in the DTP control is modified, the DTP control
 * sends the FORMAT notification.  This is what drives our program.  When the
 * user presses the up or down arrow, home, end, page up, or page down keys in
 * one of the call back fields we send a modified date and time back to the DTP
 * control.  The DTP control generates a FORMAT event, and in our FORMAT event
 * handler we return the updated value for the call back field.
 */
::method onKeyDown
  use arg field, dt, vKey, idFrom, hwndFrom

  select
    when field == 'XX' then do
      newDT = self~updatePeriodNumber(dt, vKey)
    end

    when field == 'XXX' then do
      newDT = self~updatePeriod(dt, vKey)
    end

    otherwise do
      newDT = self~updateReport(dt, vKey)
    end
  end
  -- End select

  return newDT


/** onRbClick()
 *
 * This is the event handler for the click event of a radio button.  We
 * connected the event for every radio button to this one method.  Each time the
 * user clicks on a radio button, it selects a new report type.  In our program,
 * when a new report type is selected, the initial date for the report is set to
 * the first day of the current period.  I.e., if the current period is 'month'
 * and the date is 7/10/2008, then the first day of the period is 7/1/2008.
 *
 * As explained in the comments for the onFormat() method above, when the date
 * and time is changed in the DTP control, the control generates a FORMAT event.
 * Here we update the index to the type of report, and set the new date and
 * time.  In the FORMAT event handler, the updated report type index causes the
 * event handler to return the text for the new report type.  And the display in
 * the DTP control is automatically updated to reflect the user's report
 * selection.
 */
::method onRbClick unguarded
  expose dtp currentType
  use arg info, hwnd, id

  dt = self~getFirstDayOfPeriod

  select
    when id == .constDir[IDC_RB_BAL_SHEET] then do
      currentType = 1
      dtp~setDateTime(dt)
    end

    when id == .constDir[IDC_RB_PAYROLL] then do
      currentType = 2
      dtp~setDateTime(dt)
    end

    when id == .constDir[IDC_RB_CASH_FLOW] then do
      currentType = 3
      dtp~setDateTime(dt)
    end

    when id == .constDir[IDC_RB_INCOME_STMT] then do
      currentType = 4
      dtp~setDateTime(dt)
    end

    when id == .constDir[IDC_RB_EARNINGS] then do
      currentType = 5
      dtp~setDateTime(dt)
    end

    otherwise do
      -- Should be impossible to get here.
      nop
    end
  end
  -- End select

  return 0


/** updatePeriodNumber()
 *
 * This method is invoked during the event handler for the key down event when
 * the call back field is the number, the period number (First, second, third,
 * etc..)  The user can use the up / down arrows, home, end, page up, and page
 * down keys to cycle through the periods.  All other keys are simply ignored.
 *
 * Note that when the number changes, the period (month, quarter, etc.,) stays
 * the same. The date changes to the same relative date in the new period.  The
 * numbers wrap, i.e., if it is the 12 month and the up arrow key is used, the
 * number wraps to 1.  To determine when to wrap, we have to check the period.
 * If the period is half, then we need to wrap at 3, etc..
 */
::method updatePeriodNumber unguarded private
  expose periods currentPeriod
  use strict arg dt, vkey

  mn = dt~month

  parse value dt~isoDate with pre 6 j 8 tail

  select
    when vKey == .VK~UP then do

      select
        when periods[currentPeriod] == 'month' then do
          mn += 1
          if mn == 13 then mn = 1
        end

        when periods[currentPeriod] == 'quarter' then do
          if mn <= 9 then mn += 3
          else mn -= 9
        end

        otherwise do
          if mn <= 6 then mn += 6
          else mn -= 6
        end
      end
      -- End select

      mn = mn~right(2, 0)
      newDT = .DateTime~fromIsoDate(pre || mn || tail)
    end

    when vKey == .VK~Down then do

      select
        when periods[currentPeriod] == 'month' then do
          mn -= 1
          if mn == 0 then mn = 12
        end

        when periods[currentPeriod] == 'quarter' then do
          if mn <= 3 then mn += 9
          else mn -= 3
        end

        otherwise do
          if mn <= 6 then mn += 6
          else mn -= 6
        end
      end
      -- End select

      mn = mn~right(2, 0)
      newDT = .DateTime~fromIsoDate(pre || mn || tail)
    end

    when vKey == .VK~HOME then do

      select
        when periods[currentPeriod] == 'month' then do
          mn = 1
        end

        when periods[currentPeriod] == 'quarter' then do
          if mn <= 3 then nop
          else if mn <= 6 then mn -= 3
          else if mn <= 9 then mn -= 6
          else mn -= 9
        end

        otherwise do
          if mn > 6 then mn -= 6
        end
      end
      -- End select

      mn = mn~right(2, 0)
      newDT = .DateTime~fromIsoDate(pre || mn || tail)
    end

    when vKey == .VK~END then do

      select
        when periods[currentPeriod] == 'month' then do
          mn = 12
        end

        when periods[currentPeriod] == 'quarter' then do
          if mn <= 3 then mn += 9
          else if mn <= 6 then mn += 6
          else if mn <= 9 then mn += 6
          else nop
        end

        otherwise do
          if mn <= 6 then mn += 6
        end
      end
      -- End select

      mn = mn~right(2, 0)
      newDT = .DateTime~fromIsoDate(pre || mn || tail)
    end

    when vKey == .VK~PRIOR then do
      -- We want to go half the year back, so it doesn't make any difference
      -- what the period is.
      if mn <= 6 then mn = 1
      else mn -= 6

      mn = mn~right(2, 0)
      newDT = .DateTime~fromIsoDate(pre || mn || tail)
    end

    when vKey == .VK~NEXT then do
      -- We want to go half the year ahead, so it doesn't make any difference
      -- what the period is.
      if mn <= 6 then mn += 6
      else mn = 12

      mn = mn~right(2, 0)
      newDT = .DateTime~fromIsoDate(pre || mn || tail)
    end

    otherwise do
      newDT = dt
    end
  end
  -- End select

  return newDT


/** onPrint()
 *
 * The event handler for the CLICK event for the 'Print' push button.  A real
 * program would of course print the report using the details specified by the
 * user through the DTP control.  This is just an example program, we simply
 * put up a message box.
 */
::method onPrint unguarded
  expose types currentType dtp
  use arg info, hwndFrom

  select
    when currentType == 1 then report = 'Balance Sheet'
    when currentType == 2 then report = 'Payroll'
    when currentType == 3 then report = 'Cash Flow'
    when currentType == 4 then report = 'Income Statement'
    when currentType == 5 then report = 'Earnings'
    otherwise report = 'Error Report NOT Found'
  end
  -- End select

  dt = dtp~getDateTime

  if report~word(1) == 'Error' then do
    title = report
    msg   = 'An internal software error has occurred preventing'.endOfLine     || -
            'the printing of the report.' || .endOfLine~copies(2)              || -
            'Please contact IT at "internalsupport@acmewidgets.com"'.endOfLine || -
            'to resolve this issue.'

    icon = 'ERROR'
  end
  else do
    title = "Printing the" report "Report"
    msg   = 'The' report 'report is being sent to the default'.endOfLine    || -
            'printer' || .endOfLine~copies(3)                               || -
            'Report Start Time:' || '09'x || dt~civilTime  || .endOfLine    || -
            'Report Start Date:' || '09'x || dt~languageDate

    icon = 'INFORMATION'
  end

  j = MessageDialog(msg, self~hwnd, title, 'OK', icon)


/** updatePeriod()
 *
 * This method is invoked during the event handler for the key down event when
 * the call back field is the period (month, quarter, half.)  The user can use
 * the up / down arrows, home, end, page up, and page down keys to cycle through
 * the periods.  All other keys are simply ignored.
 *
 * Note that when the period changes, the date stays the same. But, if we simply
 * return the same date, the DTP will not update its display. So, we add 1
 * second to the date, which in turn causes the DTP control to update the
 * display.
 */
::method updatePeriod unguarded private
  expose periods currentPeriod
  use strict arg dt, vkey

  select
    when vKey == .VK~UP then do
      currentPeriod += 1
      if currentPeriod > periods~items then currentPeriod = 1
      newDT = dt~addSeconds(1)
    end

    when vKey == .VK~Down then do
      currentPeriod -= 1
      if currentPeriod < 1 then currentPeriod = periods~items
      newDT = dt~addseconds(1)
    end

    -- Since there are only 3 periods, Page Up and Home will act the same.
    when vKey == .VK~PRIOR | vKey == .VK~HOME then do
      currentPeriod = 1
      newDT = dt~addSeconds(1)
    end

    -- Since there are only 3 periods, Page Down and End will act the same.
    when vKey == .VK~NEXT | vKey == .VK~END then do
      currentPeriod = periods~items
      newDT = dt~addseconds(1)
    end

    otherwise do
      newDT = dt
    end
  end
  -- End select

  return newDT


/** updateReport()
 *
 * This method is invoked during the event handler for the key down event when
 * the call back field is the report type (balance, cash flow, etc...)  The user
 * can use the up / down arrows, home, end, page up, and page down keys to cycle
 * through the periods.  All other keys are simply ignored.
 *
 * Note that when the report type changes, the date gets set to the first day of
 * the period, and the selected radio button gets changed.  For the user, the
 * effect of using one of the arrow keys in the report type call back field is
 * exactly the same as selecting a report by using the radio buttons.
 */
::method updateReport unguarded private
  expose types currentType
  use strict arg dt, vkey

  select
    when vKey == .VK~UP then do
      currentType += 1
      if currentType > types~items then currentType = 1

      newDT = self~getFirstDayOfPeriod

      .RadioButton~checkInGroup(self, .constDir[IDC_RB_PRE_FIRST] + 1,    -
                                      .constDir[IDC_RB_ANTE_LAST] - 1,    -
                                      .constDir[IDC_RB_PRE_FIRST] + currentType)
    end

    when vKey == .VK~Down then do
      currentType -= 1
      if currentType < 1 then currentType = types~items

      newDT = self~getFirstDayOfPeriod

      .RadioButton~checkInGroup(self, .constDir[IDC_RB_PRE_FIRST] + 1,    -
                                      .constDir[IDC_RB_ANTE_LAST] - 1,    -
                                      .constDir[IDC_RB_PRE_FIRST] + currentType)
    end

    otherwise do
      newDT = dt
    end
  end
  -- End select

  return newDT


/** getFirstDay()
 *
 * Returns the first day of the current period.  I.e., if the current period is
 * second half 2010, the first day is 6/1/2010
 */
::method getFirstDay private
  expose periods currentPeriod
  use strict arg dtpSelected

  yr = dtpSelected~year
  mn = dtpSelected~month

  select
    when periods[currentPeriod] == 'month' then do
      mn = mn~right(2, 0)
    end

    when periods[currentPeriod] == 'quarter' then do
      if mn <= 3 then mn = 01
      else if mn <= 6 then mn = 04
      else if mn <= 9 then mn = 07
      else mn = 10
    end

    otherwise do
      if mn <= 6 then mn = 01
      else mn = 06
    end
  end
  -- End select

  return .DateTime~fromISODate(yr'-'mn'-01T08:00:00.000000')


/** getPeriod()
 *
 * Returns the number word (first, second, etc., for the specified date based on
 * the current period.  I.e., if the date is 7/4/1999 and current period is
 * month then the number word is Seventh.
 */
::method getPeriodNumber unguarded private
  expose numbers periods currentPeriod
  use strict arg dt

  mn = dt~month

  select
    when periods[currentPeriod] == 'month' then do
      word = numbers[mn]
    end

    when periods[currentPeriod] == 'quarter' then do
      if mn <= 3 then word = numbers[1]
      else if mn <= 6 then word = numbers[2]
      else if mn <= 9 then word = numbers[3]
      else word = numbers[4]
    end

    otherwise do
      if mn <= 6 then word = numbers[1]
      else word = numbers[2]
    end
  end
  -- End select

  return word


/** getFirstDayOfPeriod()
 *
 * Returns a .DateTime object that represents the first day of the period.  The
 * first day of the period is based on the currently selected date in the DTP
 * control and the setting of the period.  I.e., if the date is 5/13/2003 and
 * the period is quarter, then the first day of the period is 4/1/2003.
 *
 * Note that the .DateTime object returned here is used to update the DTP
 * control, where we need the DTP control to generate a FORMAT event.  If the
 * returned date is exactly the same as the currently selected date, the DTP
 * control does nothing.  So, we check for that possibility and add 1 second to
 * the return.  This is enough to force the FORMAT event notification we need.
 */
::method getFirstDayOfPeriod private
  expose dtp
  use strict arg

  current = dtp~getDateTime
  dt = self~getFirstDay(current)

  if dt == current then do
    dt = dt~addSeconds(1)
  end

  return dt


/** calcSize()
 *
 * Calculates the size needed to fully display the largest string in the
 * specified call back field.  The DTP control uses this size to ensure that
 * the display is formatted correctly.
 *
 * For the specified call back field, we iterate over the items for the field,
 * calculate the size in pixels needed for the item, and save the biggest size.
 * We could have just eye-balled the biggest item in the field and calculated
 * its size.  For instance, 'quarter' looks to be the biggest item in the period
 * call back.  However, doing it this way ensures we get the correct display
 * size and allows the program to be enhanced by adding items to any of the call
 * back fields without having to change this method.
 */
::method calcSize unguarded private
  expose dtp periods numbers types
  use strict arg which

  biggest = .Size~new(0, 0)
  size   = .Size~new

  select
    when which == 'XX' then array = numbers
    when which == 'XXX' then array = periods
    otherwise array = types
  end
  -- End select

  do text over array
    dtp~textSize(text, size)
    if biggest < size then biggest~equateTo(size)
  end

  return biggest

/** setUpArrays()
 *
 * Simple house keeping, initialize an array of items for each call back field.
 */
::method setUpArrays private
  expose periods numbers types currentPeriod currentType

  periods = .Array~of('month', 'quarter', 'half')

  numbers = .Array~of('First',    'Second',  'Third',  'Fourth', 'Fifth', -
                      'Sixth',    'Seventh', 'Eighth', 'Ninth',  'Tenth', -
                      'Eleventh', 'Twelfth')

  types = .Array~of('balance', 'payroll', 'cash flow', 'income', 'earnings')

  -- Set the starting period, 'quarter', and report, 'balance'.
  currentPeriod = 2
  currentType = 1
