
module Fugit

  # A natural language set of parsers for fugit.
  # Focuses on cron expressions. The rest is better left to Chronic and friends.
  #
  module Nat

    class << self

      def parse(s, opts={})

        return s if s.is_a?(Fugit::Cron) || s.is_a?(Fugit::Duration)

        return nil unless s.is_a?(String)

#p s; Raabro.pp(Parser.parse(s, debug: 3), colours: true)
#(p s; Raabro.pp(Parser.parse(s, debug: 1), colours: true)) rescue nil
        parse_crons(s, Parser.parse(s), opts)
      end

      def do_parse(s, opts={})

        parse(s, opts) ||
        fail(ArgumentError.new("could not parse a nat #{s.inspect}"))
      end

      protected

      def parse_crons(s, a, opts)

#p a
        return nil unless a

        h = a
          .reverse
          .inject({}) { |r, e| send("parse_#{e[0]}_elt", e, opts, r); r }
            #
            # the reverse ensure that in "every day at five", the
            # "at five" is placed before the "every day" so that
            # parse_x_elt calls have the right sequence
#p h

        if f = h[:_fail]
          #fail ArgumentError.new(f)
          return nil
        end

        hms = h[:hms]

        hours = (hms || [])
          .uniq
          .inject({}) { |r, hm| (r[hm[1]] ||= []) << hm[0]; r }
          .inject({}) { |r, (m, hs)| (r[hs.sort] ||= []) << m; r }
          .to_a
          .sort_by { |hs, ms| -hs.size }
        if hours.empty?
          hours << (h[:dom] ? [ [ '0' ], [ '0' ] ] : [ [ '*' ], [ '*' ] ])
        end

        crons = hours
          .collect { |hm| assemble_cron(h.merge(hms: hm)) }

        fail ArgumentError.new(
          "multiple crons in #{s.inspect} " +
          "(#{crons.collect(&:original).join(' | ')})"
        ) if opts[:multi] == :fail && crons.size > 1

        if opts[:multi] == true || (opts[:multi] && opts[:multi] != :fail)
          crons
        else
          crons.first
        end
      end

      def assemble_cron(h)

#puts "ac: " + h.inspect
        s = []
        s << h[:sec] if h[:sec]
        s << h[:hms][1].join(',')
        s << h[:hms][0].join(',')
        s << (h[:dom] || '*') << (h[:mon] || '*') << (h[:dow] || '*')
        s << h[:tz] if h[:tz]

        Fugit::Cron.parse(s.join(' '))
      end

      def eone(e); e1 = e[1]; e1 == 1 ? '*' : "*/#{e1}"; end

      def parse_interval_elt(e, opts, h)

        e1 = e[1]

        case e[2]
        when 's', 'sec', 'second', 'seconds'
          h[:sec] = eone(e)
        when 'm', 'min', 'mins', 'minute', 'minutes'
          h[:hms] ||= [ [ '*', eone(e) ] ]
        when 'h', 'hour', 'hours'
          hms = h[:hms]
          if hms && hms.size == 1 && hms.first.first == '*'
            hms.first[0] = eone(e)
          elsif ! hms
            h[:hms] = [ [ eone(e), 0 ] ]
          end
        when 'd', 'day', 'days'
          h[:dom] = "*/#{e1}" if e1 > 1
          h[:hms] ||= [ [ 0, 0 ] ]
        when 'w', 'week', 'weeks'
          h[:_fail] = "cannot have crons for \"every #{e1} weeks\"" if e1 > 1
          h[:hms] ||= [ [ 0, 0 ] ]
          h[:dow] ||= 0
        when 'M', 'month', 'months'
          h[:_fail] = "cannot have crons for \"every #{e1} months\"" if e1 > 12
          h[:hms] ||= [ [ 0, 0 ] ]
          h[:dom] = 1
          h[:mon] = eone(e)
        when 'Y', 'y', 'year', 'years'
          h[:_fail] = "cannot have crons for \"every #{e1} years\"" if e1 > 1
          h[:hms] ||= [ [ 0, 0 ] ]
          h[:dom] = 1
          h[:mon] = 1
        end
      end

      def parse_dow_list_elt(e, opts, h)

        h[:hms] ||= [ [ 0, 0 ] ]
        h[:dow] = e[1..-1].collect(&:to_s).sort.join(',')
      end

      def parse_dow_range_elt(e, opts, h)

        h[:hms] ||= [ [ 0, 0 ] ]
        h[:dow] = e[1] == e[2] ? e[1] : "#{e[1]}-#{e[2]}"
      end

      def parse_day_of_month_elt(e, opts, h)

        h[:dom] = e[1..-1].join(',')
      end

      def parse_at_elt(e, opts, h)

        (h[:hms] ||= []).concat(e[1])

        l = h[:hms].last
        h[:sec] = l.pop if l.size > 2
      end

      def parse_on_elt(e, opts, h)

        e1 = e[1]
        h[:dow] = e1[0]
        h[:hms] = [ e1[1] ]

        l = h[:hms].last
        h[:sec] = l.pop if l.size > 2
      end

      def parse_tz_elt(e, opts, h)

        h[:tz] = e[1]
      end
    end

    module Parser include Raabro

      NUMS = %w[
        zero one two three four five six seven eight nine ten eleven twelve ]

      WEEKDAYS =
        Fugit::Cron::Parser::WEEKDS + Fugit::Cron::Parser::WEEKDAYS

      NHOURS = {
        'noon' => [ 12, 0 ],
        'midnight' => [ 0, 0 ], 'oh' => [ 0, 0 ] }
      NMINUTES = {
        "o'clock" => 0, 'five' => 5,
        'ten' => 10, 'fifteen' => 15,
        'twenty' => 20, 'twenty-five' => 25,
        'thirty' => 30, 'thirty-five' => 35,
        'fourty' => 40, 'fourty-five' => 45,
        'fifty' => 50, 'fifty-five' => 55 }

      oh = {
        '1st' => 1, '2nd' => 2, '3rd' => 3, '21st' => 21, '22nd' => 22,
        '23rd' => 23, '31st' => 31 }
      %w[ 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 24 25 26 27 28 29 30 ]
        .each { |i| oh["#{i}th"] = i.to_i }
      %w[
        first second third fourth fifth sixth seventh eighth ninth tenth
        eleventh twelfth thirteenth fourteenth fifteenth sixteenth seventeenth
        eighteenth nineteenth twentieth twenty-first twenty-second twenty-third
        twenty-fourth twenty-fifth twenty-fifth twenty-sixth twenty-seventh
        twenty-eighth twenty-ninth thirtieth thirty-first ]
          .each_with_index { |e, i| oh[e] = i + 1 }
      ORDINALS = oh

      # piece parsers bottom to top

      def _from(i); rex(nil, i, /\s*from\s+/i); end
      def _every(i); rex(nil, i, /\s*(every)\s+/i); end
      def _at(i); rex(nil, i, /\s*at\s+/i); end
      def _in(i); rex(nil, i, /\s*(in|on)\s+/i); end
      def _to(i); rex(nil, i, /\s*to\s+/i); end
      def _dash(i); rex(nil, i, /-\s*/i); end
      def _and(i); rex(nil, i, /\s*and\s+/i); end
      def _on(i); rex(nil, i, /\s*on\s+/i); end

      def _and_or_comma(i)
        rex(nil, i, /\s*(,?\s*and\s|,?\s*or\s|,)\s*/i)
      end
      def _at_comma(i)
        rex(nil, i, /\s*(at\s|,|)\s*/i)
      end
      def _to_through(i)
        rex(nil, i, /\s*(to|through)\s+/i)
      end

      def integer(i); rex(:int, i, /\d+\s*/); end

      def tz_name(i)
        rex(nil, i,
          /\s*[A-Z][a-zA-Z0-9+\-]+(\/[A-Z][a-zA-Z0-9+\-_]+){0,2}(\s+|$)/)
      end
      def tz_delta(i)
        rex(nil, i,
          /\s*[-+]([01][0-9]|2[0-4]):?(00|15|30|45)(\s+|$)/)
      end
      def tzone(i)
        alt(:tzone, i, :tz_delta, :tz_name)
      end

      def and_named_digits(i)
rex(:xxx, i, 'TODO')
      end

      def dname(i)
        rex(:dname, i, /(s(ec(onds?)?)?|m(in(utes?)?)?)\s+/i)
      end
      def named_digit(i)
        seq(:named_digit, i, :dname, :integer)
      end
      def named_digits(i)
        seq(nil, i, :named_digit, '+', :and_named_digits, '*')
      end

      def am_pm(i)
        rex(:am_pm, i, /\s*(am|pm|dark)\s*/i)
      end

      def nminute(i)
        rex(:nminute, i, /(#{NMINUTES.keys.join('|')})\s*/i)
      end
      def nhour(i)
        rex(:nhour, i, /(#{NUMS.join('|')})\s*/i)
      end
      def numeral_hour(i)
        seq(:numeral_hour, i, :nhour, :am_pm, '?', :nminute, '?')
      end

      def named_hour(i)
        rex(:named_hour, i, /(#{NHOURS.keys.join('|')})/i)
      end

      def shour(i)
        rex(:shour, i, /(2[0-4]|[01]?[0-9])/)
      end
      def simple_hour(i)
        seq(:simple_hour, i, :shour, :am_pm, '?')
      end

      def dig_hour_b(i); rex(nil, i, /(2[0-4]|[01][0-9]|[0-9]):[0-5]\d/); end
      def dig_hour_a(i); rex(nil, i, /(2[0-4]|[01][0-9])[0-5]\d/); end
      def dig_hour(i); alt(nil, i, :dig_hour_a, :dig_hour_b); end
        #
      def digital_hour(i)
        seq(:digital_hour, i, :dig_hour, :am_pm, '?')
      end

      def at_point(i)
        alt(nil, i,
          :digital_hour, :simple_hour, :named_hour, :numeral_hour,
          :named_digits)
      end

      def weekday(i)
        rex(:weekday, i, /(#{WEEKDAYS.reverse.join('|')})\s*/i)
      end

      def and_at(i)
        seq(nil, i, :_and_or_comma, :at_point)
      end

      def _intervals(i)
        rex(:intervals, i,
          /(
            y(ears?)?|months?|w(eeks?)?|d(ays?)?|
            h(ours?)?|m(in(ute)?s?)?|s(ec(ond)?s?)?
          )(\s+|$)/ix)
      end

      def sinterval(i)
        rex(:sinterval, i,
          /(year|month|week|day|hour|min(ute)?|sec(ond)?)(\s+|$)/i)
      end
      def ninterval(i)
        seq(:ninterval, i, :integer, :_intervals)
      end

      def ordinal(i)
        rex(:ordinal, i, /\s*(#{ORDINALS.keys.join('|')})\s*/)
      end

      def _mod(i); rex(nil, i, /\s*month\s+on\s+days?\s+/i); end
      def _oftm(i); rex(nil, i, /\s*(day\s)?\s*of\s+the\s+month\s*/i); end

      def dom(i)
        rex(:int, i, /([12][0-9]|3[01]|[0-9])/)
      end
      def and_or_dom(i)
        seq(nil, i, :_and_or_comma, :dom)
      end
      def dom_list(i)
        seq(:dom_list, i, :dom, :and_or_dom, '*')
      end

      def dom_mod(i) # every month on day
        seq(:dom, i, :_mod, :dom_list)
      end
      def dom_noftm(i) # every nth of month
        seq(:dom, i, :ordinal, :_oftm)
      end
      def day_of_month(i)
        alt(nil, i, :dom_noftm, :dom_mod)
      end

      def dow_class(i)
        rex(:dow_class, i, /(weekday)(\s+|$)/i)
      end

      def dow(i)
        seq(:dow, i, :weekday)
      end
      def and_or_dow(i)
        seq(nil, i, :_and_or_comma, :dow)
      end
      def dow_list(i)
        seq(:dow_list, i, :dow, :and_or_dow, '*')
      end

      def to_dow_range(i)
        seq(:dow_range, i, :weekday, :_to_through, :weekday)
      end
      def dash_dow_range(i)
        seq(:dow_range, i, :weekday, :_dash, :weekday)
      end
      def dow_range(i)
        alt(nil, i, :to_dow_range, :dash_dow_range)
      end

      def day_of_week(i)
        alt(nil, i, :dow_range, :dow_list, :dow_class)
      end

      def interval(i)
        alt(nil, i, :sinterval, :ninterval)
      end

      def every_object(i)
        alt(nil, i, :day_of_month, :interval, :day_of_week)
      end
      def from_object(i)
        alt(nil, i, :interval, :to_dow_range)
      end

      def tz(i)
        seq(nil, i, :_in, '?', :tzone)
      end
      def on(i)
        seq(:on, i, :_on, :weekday, :at_point, :and_at, '*')
      end
      def at(i)
        seq(:at, i, :_at_comma, :at_point, :and_at, '*')
      end
      def from(i)
        seq(:from, i, :_from, :from_object)
      end
      def every(i)
        seq(:every, i, :_every, :every_object)
      end

      def at_from(i)
        seq(nil, i, :at, :from, :tz, '?')
      end
      def at_every(i)
        seq(nil, i, :at, :every, :tz, '?')
      end

      def from_at(i)
        seq(nil, i, :from, :at, '?', :tz, '?')
      end

      def every_(i)
        seq(nil, i, :every, :tz, '?')
      end
      def every_on(i)
        seq(nil, i, :every, :on, :tz, '?')
      end
      def every_at(i)
        seq(nil, i, :every, :at, :tz, '?')
      end

      def nat(i)
        alt(:nat, i,
          :every_at, :every_on, :every_,
          :from_at,
          :at_every, :at_from)
      end

      # rewrite parsed tree

      #def _rewrite_single(t)
      #  [ t.name, rewrite(t.sublookup(nil)) ]
      #end
      def _rewrite_children(t)
        t.subgather(nil).collect { |tt| rewrite(tt) }
      end
      def _rewrite_multiple(t)
        [ t.name, _rewrite_children(t) ]
      end
      def _rewrite_child(t)
        rewrite(t.sublookup(nil))
      end

      def rewrite_int(t); t.string.to_i; end

      def rewrite_tzone(t)

        [ :tz, t.strim ]
      end

      def rewrite_sinterval(t)

        [ :interval, 1, t.strim ]
      end

      def rewrite_ninterval(t)

        [ :interval,
          t.sublookup(:int).string.to_i,
          t.sublookup(:intervals).strim ]
      end

      def rewrite_named_digit(t)

        i = t.sublookup(:int).string.to_i

        case n = t.sublookup(:dname).strim
        when /^s/ then [ '*', '*', i ]
        when /^m/ then [ '*', i ]
        end
      end

      def rewrite_named_hour(t)
        NHOURS[t.strim.downcase]
      end
      def rewrite_numeral_hour(t)
        vs = t.subgather(nil).collect { |st| st.strim.downcase }
        v = NUMS.index(vs[0])
        v += 12 if vs[1] == 'pm'
        m = NMINUTES[vs[2]] || 0
        [ v, m ]
      end
      def rewrite_simple_hour(t)
        vs = t.subgather(nil).collect { |st| st.strim.downcase }
        v = vs[0].to_i
        v += 12 if vs[1] == 'pm'
        [ v, 0 ]
      end
      def rewrite_digital_hour(t)
        m = t.string.match(/(\d\d?):?(\d\d)(\s+pm)?/i)
        hou = m[1].to_i; hou += 12 if m[3] && hou < 12
        min = m[2].to_i
        [ hou, min ]
      end

      def rewrite_weekday(t)

        WEEKDAYS.index(t.strim.downcase[0, 3])
      end

      def rewrite_ordinal(t); ORDINALS[t.strim]; end

      def rewrite_dom(t)

#Raabro.pp(t, colours: true)
        [ :day_of_month,
          *_rewrite_children(t).flatten.select { |e| e.is_a?(Integer) } ]
      end

      alias rewrite_dow _rewrite_child

      def rewrite_dom_list(t); [ :dom_list, *_rewrite_children(t) ]; end
      def rewrite_dow_list(t); [ :dow_list, *_rewrite_children(t) ]; end

      def rewrite_dow_class(t)

        [ :dow_range, 1, 5 ] # only "weekday" for now
      end

      def rewrite_dow_range(t)

        tts = t.subgather(nil)

        [ :dow_range, rewrite(tts[0]), rewrite(tts[1]) ]
      end

      alias rewrite_on _rewrite_multiple
      alias rewrite_at _rewrite_multiple

      alias rewrite_from _rewrite_child
      alias rewrite_every _rewrite_child

      def rewrite_nat(t)

        t.subgather(nil).collect { |tt| rewrite(tt) }
#.tap { |x| pp x }
      end
    end
  end
end

