require "rsec"
require "set"
require "uri"
require "date"
require "tzinfo"
include Rsec::Helpers
require_relative "../../c"
require_relative "../../error"
require_relative "./propertyparent"
require "vobject"
require_relative "./propertyvalue"

module Vobject::Vcalendar
  class Typegrammars
    class << self
      # property value types, each defining their own parser
      def recur
        freq = /SECONDLY/i.r | /MINUTELY/i.r | /HOURLY/i.r | /DAILY/i.r |
          /WEEKLY/i.r | /MONTHLY/i.r | /YEARLY/i.r
        enddate = C::DATE_TIME | C::DATE
        seconds = /[0-9]{1,2}/.r
        byseclist = seq(seconds << ",".r, lazy { byseclist }) do |(s, l)|
          [s, l].flatten
        end | seconds.map { |s| [s] }
        minutes = /[0-9]{1,2}/.r
        byminlist = seq(minutes << ",".r, lazy { byminlist }) do |(m, l)|
          [m, l].flatten
        end | minutes.map { |m| [m] }
        hours = /[0-9]{1,2}/.r
        byhrlist = seq(hours << ",".r, lazy { byhrlist }) do |(h, l)|
          [h, l].flatten
        end | hours.map { |h| [h] }
        ordwk = /[0-9]{1,2}/.r
        weekday = /SU/i.r | /MO/i.r | /TU/i.r | /WE/i.r | /TH/i.r | /FR/i.r | /SA/i.r
        weekdaynum1 = seq(C::SIGN._?, ordwk) do |(s, o)|
          h = { ordwk: o }
          h[:sign] = s[0] unless s.empty?
          h
        end
        weekdaynum = seq(weekdaynum1._?, weekday) do |(a, b)|
          h = { weekday: b }
          h = h.merge a[0] unless a.empty?
          h
        end
        bywdaylist = seq(weekdaynum << ",".r, lazy { bywdaylist }) do |(w, l)|
          [w, l].flatten
        end | weekdaynum.map { |w| [w] }
        ordmoday = /[0-9]{1,2}/.r
        monthdaynum = seq(C::SIGN._?, ordmoday) do |(s, o)|
          h = { ordmoday: o }
          h[:sign] = s[0] unless s.empty?
          h
        end
        bymodaylist = seq(monthdaynum << ",".r, lazy { bymodaylist }) do |(m, l)|
          [m, l].flatten
        end | monthdaynum.map { |m| [m] }
        ordyrday = /[0-9]{1,3}/.r
        yeardaynum = seq(C::SIGN._?, ordyrday) do |(s, o)|
          h = { ordyrday: o }
          h[:sign] = s[0] unless s.empty?
          h
        end
        byyrdaylist = seq(yeardaynum << ",".r, lazy { byyrdaylist }) do |(y, l)|
          [y, l].flatten
        end | yeardaynum.map { |y| [y] }
        weeknum = seq(C::SIGN._?, ordwk) do |(s, o)|
          h = { ordwk: o }
          h[:sign] = s[0] unless s.empty?
          h
        end
        bywknolist = seq(weeknum << ",".r, lazy { bywknolist }) do |(w, l)|
          [w, l].flatten
        end | weeknum.map { |w| [w] }
        # monthnum = /[0-9]{1,2}/.r
        # RFC 7529 add leap month indicator
        monthnum = /[0-9]{1,2}L?/i.r
        bymolist = seq(monthnum << ",".r, lazy { bymolist }) do |(m, l)|
          [m, l].flatten
        end | monthnum.map { |m| [m] }
        setposday = yeardaynum
        bysplist = seq(setposday << ",".r, lazy { bysplist }) do |(s, l)|
          [s, l].flatten
        end | setposday.map { |s| [s] }
        # http://www.unicode.org/repos/cldr/tags/latest/common/bcp47/calendar.xml
        rscale = C::XNAME_VCAL | /buddhist/i.r | /chinese/i.r | /coptic/i.r | /dangi/i.r |
          /ethioaa/i.r | /ethiopic-amete-alem/i.r | /ethiopic/i.r |
          /gregory/i.r | /hebrew/i.r | /indian/i.r | /islamic/i.r |
          /islamic-umalqura/i.r | /islamic-tbla/i.r | /islamic-civil/i.r |
          /islamic-rgsa/i.r | /iso8601/i.r | /japanese/i.r | /persian/i.r |
          /roc/i.r | /islamicc/i.r | /gregorian/i.r
        skip = /OMIT/i.r | /BACKWARD/i.r | /FORWARD/i.r
        recur_rule_part = 	seq(/FREQ/i.r << "=".r, freq) { |(_k, v)| { freq: v } } |
          seq(/UNTIL/i.r << "=".r, enddate) { |(_k, v)| { until: v } } |
          seq(/COUNT/i.r << "=".r, /[0-9]+/i.r) { |(_k, v)| { count: v } } |
          seq(/INTERVAL/i.r << "=".r, /[0-9]+/i.r) { |(_k, v)| { interval: v } } |
          seq(/BYSECOND/i.r << "=".r, byseclist) { |(_k, v)| { bysecond: v } } |
          seq(/BYMINUTE/i.r << "=".r, byminlist) { |(_k, v)| { byminute: v } } |
          seq(/BYHOUR/i.r << "=".r, byhrlist) { |(_k, v)| { byhour: v } } |
          seq(/BYDAY/i.r << "=".r, bywdaylist) { |(_k, v)| { byday: v } } |
          seq(/BYMONTHDAY/i.r << "=".r, bymodaylist) { |(_k, v)| { bymonthday: v } } |
          seq(/BYYEARDAY/i.r << "=".r, byyrdaylist) { |(_k, v)| { byyearday: v } } |
          seq(/BYWEEKNO/i.r << "=".r, bywknolist) { |(_k, v)| { byweekno: v } } |
          seq(/BYMONTH/i.r << "=".r, bymolist) { |(_k, v)| { bymonth: v } } |
          seq(/BYSETPOS/i.r << "=".r, bysplist) { |(_k, v)| { bysetpos: v } } |
          seq(/WKST/i.r << "=".r, weekday) { |(_k, v)| { wkst: v } } |
          # RFC 7529
          seq(/RSCALE/i.r << "=".r, rscale) { |(_k, v)| { rscale: v } } |
          seq(/SKIP/i.r << "=".r, skip) { |(_k, v)| { skip: v } }
        recur1 = seq(recur_rule_part, ";", lazy { recur1 }) { |(h, _, r)| h.merge r } |
          recur_rule_part
        recur = recur1.map { |r| PropertyValue::Recur.new r }
        recur.eof
      end

      def integer
        integer = prim(:int32).map { |i| PropertyValue::Integer.new i }
        integer.eof
      end

      def percent_complete
        integer = prim(:int32).map do |a|
          if a >= 0 && a <= 100
            PropertyValue::PercentComplete.new a
          else
            { error: "Percentage outside of range 0..100" }
          end
        end
        integer.eof
      end

      def priority
        integer = prim(:int32).map do |a|
          if a >= 0 && a <= 9
            PropertyValue::Priority.new a
          else
            { error: "Percentage outside of range 0..100" }
          end
        end
        integer.eof
      end

      def float_t
        float_t = prim(:double).map { |f| PropertyValue::Float.new f }
        float_t.eof
      end

      def time_t
        time_t = C::TIME.map { |t| PropertyValue::Time.new t }
        time_t.eof
      end

      def geovalue
        float = prim(:double)
        geovalue = seq(float << ";".r, float) do |(a, b)|
          if a <= 180.0 && a >= -180.0 && b <= 180 && b > -180
            PropertyValue::Geovalue.new(lat: a, long: b)
          else
            { error: "Latitude/Longitude outside of range -180..180" }
          end
        end
        geovalue.eof
      end

      def calscalevalue
        calscalevalue = /GREGORIAN/i.r.map { PropertyValue::Calscale.new "GREGORIAN" }
        calscalevalue.eof
      end

      def iana_token
        iana_token = C::IANATOKEN.map { |x| PropertyValue::Ianatoken.new x }
        iana_token.eof
      end

      def versionvalue
        versionvalue = seq(prim(:double) << ";".r,
                           prim(:double)) do |(x, y)|
          PropertyValue::Version.new [x, y]
        end | "2.0".r.map do
          PropertyValue::Version.new ["2.0"]
        end | prim(:double).map do |v|
          PropertyValue::Version.new v
        end
        versionvalue.eof
      end

      def binary
        binary = seq(/[a-zA-Z0-9+\/]*/.r, /={0,2}/.r) do |(b, q)|
          if (b.length + q.length) % 4 == 0
            PropertyValue::Binary.new(b + q)
          else
            { error: "Malformed binary coding" }
          end
        end
        binary.eof
      end

      def uri
        uri = /\S+/.r.map do |s|
          if s =~ URI::DEFAULT_PARSER.make_regexp
            PropertyValue::Uri.new(s)
          else
            { error: "Invalid URI" }
          end
        end
        uri.eof
      end

      def text_t
        text_t = C::TEXT.map { |t| PropertyValue::Text.new(unescape(t)) }
        text_t.eof
      end

      def textlist
        textlist1 =
          seq(C::TEXT << ",".r, lazy { textlist1 }) { |(a, b)| [unescape(a), b].flatten } |
          C::TEXT.map { |t| [unescape(t)] }
        textlist = textlist1.map { |m| PropertyValue::Textlist.new m }
        textlist.eof
      end

      def request_statusvalue
        @req_status = Set.new %w{2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14 4.0 5.0 5.1 5.2 5.3}
        extdata = seq(";".r, C::TEXT) { |(_, t)| t }
        request_statusvalue = seq(/[0-9](\.[0-9]){1,2}/.r << ";".r, C::TEXT, extdata._?) do |(n, t1, t2)|
          return { error: "Invalid request status #{n}" } unless @req_status.include?(n) # RFC 5546
          hash = { statcode: n, statdesc: t1 }
          hash[:extdata] = t2[0] unless t2.empty?
          Vobject::Vcalendar::PropertyValue::Requeststatusvalue.new hash
        end
        request_statusvalue.eof
      end

      def classvalue
        classvalue = (/PUBLIC/i.r | /PRIVATE/i.r | /CONFIDENTIAL/i.r | C::XNAME_VCAL | C::IANATOKEN).map do |m|
          PropertyValue::ClassValue.new m
        end
        classvalue.eof
      end

      def eventstatus
        eventstatus = (/TENTATIVE/i.r | /CONFIRMED/i.r | /CANCELLED/i.r).map do |m|
          PropertyValue::EventStatus.new m
        end
        eventstatus.eof
      end

      def todostatus
        todostatus = (/NEEDS-ACTION/i.r | /COMPLETED/i.r | /IN-PROCESS/i.r | /CANCELLED/i.r).map do |m|
          PropertyValue::Todostatus.new m
        end
        todostatus.eof
      end

      def journalstatus
        journalstatus = (/DRAFT/i.r | /FINAL/i.r | /CANCELLED/i.r).map do |m|
          PropertyValue::Journalstatus.new m
        end
        journalstatus.eof
      end

      def date_t
        date_t = C::DATE
        date_t.eof
      end

      def datelist
        datelist1 = seq(C::DATE << ",".r, lazy { datelist1 }) do |(d, l)|
          [d, l].flatten
        end | C::DATE.map { |d| [d] }
        datelist = datelist1.map { |m| PropertyValue::Datelist.new m }
        datelist.eof
      end

      def date_time_t
        C::DATE_TIME.eof
      end

      def date_timelist
        date_timelist1 = seq(C::DATE_TIME << ",".r,
                             lazy { date_timelist1 }) do |(d, l)|
          [d, l].flatten
        end | C::DATE_TIME.map { |d| [d] }
        date_timelist = date_timelist1.map do |m|
          PropertyValue::Datetimelist.new m
        end
        date_timelist.eof
      end

      def date_time_utc_t
        date_time_utc_t = C::DATE_TIME_UTC
        date_time_utc_t.eof
      end

      def date_time_utclist
        date_time_utclist1 = seq(C::DATE_TIME_UTC << ",".r, lazy { date_time_utclist1 }) do |(d, l)|
          [d, l].flatten
        end | C::DATE_TIME_UTC.map { |d| [d] }
        date_time_utclist = date_time_utclist1.map do |m|
          PropertyValue::Datetimeutclist.new m
        end
        date_time_utclist.eof
      end

      def duration_t
        duration = C::DURATION.map { |d| PropertyValue::Duration.new d }
        duration.eof
      end

      def periodlist
        period_explicit = seq(C::DATE_TIME << "/".r, C::DATE_TIME) do |(s, e)|
          { start: s, end: e }
        end
        period_start = seq(C::DATE_TIME << "/".r, C::DURATION) do |(s, d)|
          { start: s, duration: PropertyValue::Duration.new(d) }
        end
        period = period_explicit | period_start
        periodlist1 = seq(period << ",".r, lazy { periodlist1 }) do |(p, l)|
          [p, l].flatten
        end | period.map { |p| [p] }
        periodlist = periodlist1.map { |m| PropertyValue::Periodlist.new m }
        periodlist.eof
      end

      def transpvalue
        transpvalue = (/OPAQUE/i.r | /TRANSPARENT/i.r).map do |m|
          PropertyValue::TranspValue.new m
        end
        transpvalue.eof
      end

      def utc_offset
        utc_offset = seq(C::SIGN, /[0-9]{2}/.r, /[0-9]{2}/.r,
                         /[0-9]{2}/.r._?) do |(sign, h, m, sec)|
          hash = { sign: sign, hr: h, min: m }
          hash[:sec] = sec[0] unless sec.empty?
          PropertyValue::Utcoffset.new hash
        end
        utc_offset.eof
      end

      def actionvalue
        actionvalue = (/AUDIO/i.r | /DISPLAY/i.r | /EMAIL/i.r | C::IANATOKEN |
                       C::XNAME_VCAL).map { |m| PropertyValue::ActionValue.new m }
        actionvalue.eof
      end

      def boolean
        boolean = C::BOOLEAN.map { |b| PropertyValue::Boolean.new b }
        boolean.eof
      end

      # RFC 5546
      def methodvalue
        methodvalue = (/PUBLISH/i.r | /REQUEST/i.r | /REPLY/i.r | /ADD/i.r |
                       /CANCEL/i.r | /REFRESH/i.r | /COUNTER/i.r |
                       /DECLINECOUNTER/i.r).map { |m| PropertyValue::MethodValue.new m }
        methodvalue.eof
      end

      # RFC 7953
      def busytype
        busytype = (/BUSY-UNAVAILABLE/i.r | /BUSY-TENTATIVE/i.r | /BUSY/i.r |
                    C::IANATOKEN |
                    C::XNAME_VCAL).map { |m| PropertyValue::BusyType.new m }
        busytype.eof
      end

      # https://www.w3.org/TR/2011/REC-css3-color-20110607/#svg-color
      def color
        color = C::COLOR.map { |m| PropertyValue::Color.new m }
        color.eof
      end

      # text escapes: \\ \; \, \N \n
      def unescape(x)
        # temporarily escape \\ as \007f, which is disallowed in any text
        x.gsub(/\\\\/, "\u007f").gsub(/\\;/, ";").gsub(/\\,/, ",").
          gsub(/\\[Nn]/, "\n").tr("\u007f", "\\")
      end

      def registered_propname
        registered_propname = C::NAME_VCAL
        registered_propname.eof
      end

      def registered_propname?(x)
        p = registered_propname.parse(x)
        not(Rsec::INVALID[p])
      end

      # Enforce type restrictions on values of particular properties.
      # If successful, return typed interpretation of string
      def typematch(strict, key, params, component, value, ctx)
        errors = []
        errors << property_parent(strict, key, component, value, ctx)
        ctx1 = Rsec::ParseContext.new value, "source"
        case key
        when :CALSCALE
          ret = calscalevalue._parse ctx1
        when :METHOD
          ret = methodvalue._parse ctx1
        when :VERSION
          ret = versionvalue._parse ctx1
        when :ATTACH
          ret = if params[:VALUE] == "BINARY"
                  binary._parse ctx1
                else
                  uri._parse ctx1
                end
        when :IMAGE
          parse_err(strict, errors, "No VALUE parameter specified for property #{key}", ctx1) if params.empty?
          parse_err(strict, errors, "No VALUE parameter specified for property #{key}", ctx1) unless params[:VALUE]
          if params[:VALUE] == "BINARY"
            parse_err(strict, errors, "No ENCODING parameter specified for property #{key}", ctx1) unless params[:ENCODING]
            parse_err(strict, errors, "Incorrect ENCODING parameter specified for property #{key}", ctx1) unless params[:ENCODING] == "BASE64"
            ret = binary._parse ctx1
          elsif params[:VALUE] == "URI"
            ret = uri._parse ctx1
          else
            parse_err(strict, errors, "Incorrect VALUE parameter specified for property #{key}", ctx1)
          end
        when :CATEGORIES, :RESOURCES
          ret = textlist._parse ctx1
        when :CLASS
          ret = classvalue._parse ctx1
        when :COMMENT, :DESCRIPTION, :LOCATION, :SUMMARY, :TZID, :TZNAME,
          :CONTACT, :RELATED_TO, :UID, :PRODID, :NAME
          ret = text_t._parse ctx1
        when :GEO
          ret = geovalue._parse ctx1
        when :PERCENT_COMPLETE
          ret = percent_complete._parse ctx1
        when :PRIORITY
          ret = priority._parse ctx1
        when :STATUS
          ret = case component
                when :EVENT
                  eventstatus._parse ctx1
                when :TODO
                  todostatus._parse ctx1
                when :JOURNAL
                  journalstatus._parse ctx1
                else
                  text_t._parse ctx1
                end
        when :COMPLETED, :CREATED, :DTSTAMP, :LAST_MODIFIED
          ret = date_time_utc_t._parse ctx1
        when :DTEND, :DTSTART, :DUE, :RECURRENCE_ID
          if params && params[:VALUE] == "DATE"
            ret = date_t._parse ctx1
          elsif component == :FREEBUSY
            ret = date_time_utc_t._parse ctx1
          elsif params && params[:TZID]
            if [:STANDARD || :DAYLIGHT].include? component
              parse_err(strict, errors, "Specified TZID within property #{key} in #{component}", ctx1)
            end
            begin
              tz = TZInfo::Timezone.get(params[:TZID])
              ret = date_time_t._parse ctx1
              # note that we use the registered tz information to map to UTC, rather than look up the values witin the VTIMEZONE component
              ret.value[:time] = tz.local_to_utc(ret.value[:time])
              ret.value[:zone] = params[:TZID]
            rescue
              # undefined timezone: default to floating local
              ret = date_time_t._parse ctx1
            end
          else
            ret = date_time_t._parse ctx1
          end
        when :EXDATE
          if params && params[:VALUE] == "DATE"
            ret = datelist._parse ctx1
          elsif params && params[:TZID]
            if [:STANDARD || :DAYLIGHT].include? component
              parse_err(strict, errors, "Specified TZID within property #{key} in #{component}", ctx1)
            end
            tz = TZInfo::Timezone.get(params[:TZID])
            ret = date_timelist._parse ctx1
            ret.value.each do |x|
              x.value[:time] = tz.local_to_utc(x.value[:time])
              x.value[:zone] = params[:TZID]
            end
          else
            ret = date_timelist._parse ctx1
          end
        when :RDATE
          if params && params[:VALUE] == "DATE"
            ret = datelist._parse ctx1
          elsif params && params[:VALUE] == "PERIOD"
            ret = periodlist._parse ctx1
          elsif params && params[:TZID]
            if [:STANDARD || :DAYLIGHT].include? component
              parse_err(strict, errors, "Specified TZID within property #{key} in #{component}", ctx1)
            end
            tz = TZInfo::Timezone.get(params[:TZID])
            ret = date_timelist._parse ctx1
            ret.value.each do |x|
              x.value[:time] = tz.local_to_utc(x.value[:time])
              x.value[:zone] = params[:TZID]
            end
          else
            ret = date_timelist._parse ctx1
          end
        when :TRIGGER
          if params && params[:VALUE] == "DATE-TIME" || /^\d{8}T/.match(value)
            if params && params[:RELATED]
              parse_err(strict, errors, "Specified RELATED within property #{key} as date-time", ctx1)
            end
            ret = date_time_utc_t._parse ctx1
          else
            ret = duration_t._parse ctx1
          end
        when :FREEBUSY
          ret = periodlist._parse ctx1
        when :TRANSP
          ret = transpvalue._parse ctx1
        when :TZOFFSETFROM, :TZOFFSETTO
          ret = utc_offset._parse ctx1
        when :TZURI, :URL, :SOURCE, :CONFERENCE
          if key == :CONFERENCE
            parse_err(strict, errors, "Missing URI VALUE parameter", ctx1) if params.empty?
            parse_err(strict, errors, "Missing URI VALUE parameter", ctx1) if !params[:VALUE]
            parse_err(strict, errors, "report_error Type mismatch of VALUE parameter #{params[:VALUE]} for property #{key}", ctx1) if params[:VALUE] != "URI"
          end
          ret = uri._parse ctx1
        when :ATTENDEE, :ORGANIZER
          ret = uri._parse ctx1
        when :RRULE
          ret = recur._parse ctx1
        when :ACTION
          ret = actionvalue._parse ctx1
        when :REPEAT, :SEQUENCE
          ret = integer._parse ctx1
        when :REQUEST_STATUS
          ret = request_statusvalue._parse ctx1
          # RFC 7953
        when :BUSYTYPE
          ret = busytype._parse ctx1
          # RFC 7986
        when :REFRESH_INTERVAL
          parse_err(strict, errors, "Missing VALUE parameter for property #{key}", ctx1) if params.empty?
          parse_err(strict, errors, "Missing VALUE parameter for property #{key}", ctx1) if !params[:VALUE]
          parse_err(strict, errors, "Type mismatch of VALUE parameter #{params[:VALUE]} for property #{key}", ctx1) if params[:VALUE] != "DURATION"
          ret = duration_t._parse ctx1
          # RFC 7986
        when :COLOR
          ret = color._parse ctx1
        else
          if params && params[:VALUE]
            case params[:VALUE]
            when "BOOLEAN"
              ret = boolean._parse ctx1
            when "BINARY"
              ret = binary._parse ctx1
            when "CAL-ADDRESS"
              ret = uri._parse ctx1
            when "DATE-TIME"
              ret = date_time_t._parse ctx1
            when "DATE"
              ret = date_t._parse ctx1
            when "DURATION"
              ret = duration_t._parse ctx1
            when "FLOAT"
              ret = float_t._parse ctx1
            when "INTEGER"
              ret = integer._parse ctx1
            when "PERIOD"
              ret = period._parse ctx1
            when "RECUR"
              ret = recur._parse ctx1
            when "TEXT"
              ret = text_t._parse ctx1
            when "TIME"
              ret = time_t._parse ctx1
            when "URI"
              ret = uri._parse ctx1
            when "UTC-OFFSET"
              ret = utc_offset._parse ctx1
            end
          else
            ret = text_t._parse ctx1
          end
        end
        if ret.is_a?(Hash) && ret[:error]
          parse_err(strict, errors, "#{ret[:error]} for property #{key}, value #{value}", ctx)
        end
        if Rsec::INVALID[ret]
          parse_err(strict, errors, "Type mismatch for property #{key}, value #{value}", ctx)
        end
        Rsec::Fail.reset
        [ret, errors]
      end

      private

      def parse_err(strict, errors, msg, ctx)
        if strict
          raise ctx.report_error msg, "source"
        else
          errors << ctx.report_error(msg, "source")
        end
      end
    end
  end
end
