# frozen_string_literal: true

# Ruby Object representation of an hl7 2.x message segment
# The segments can be setup to provide aliases to specific fields with
# optional validation code that is run when the field is modified
# The segment field data is also accessible via the e<number> method.
#
# == Defining a New Segment
#  class HL7::Message::Segment::NK1 < HL7::Message::Segment
#    weight 100 # segments are sorted ascendingly
#    add_field :something_you_want       # assumes :idx=>1
#    add_field :something_else, :idx=>6  # :idx=>6 and field count=6
#    add_field :something_more           # :idx=>7
#    add_field :block_example do |value|
#       raise HL7::InvalidDataError.new
#                                  unless value.to_i < 100 && value.to_i > 10
#      return value
#    end
#    # this block will be executed when seg.block_example= is called
#    # and when seg.block_example is called
#
class HL7::Message::Segment
  extend HL7::Message::SegmentListStorage
  extend TimeFormatterHelper

  include HL7::Message::SegmentFields

  attr_accessor :segment_parent
  attr_reader :element_delim, :item_delim, :segment_weight

  METHOD_MISSING_FOR_INITIALIZER = <<-END
    def method_missing( sym, *args, &blk )
      __seg__.send( sym, args, blk )
    end
  END

  # setup a new HL7::Message::Segment
  # raw_segment:: is an optional String or Array which will be used as the
  #               segment's field data
  # delims:: an optional array of delimiters, where
  #               delims[0] = element delimiter
  #               delims[1] = item delimiter
  def initialize(raw_segment = "", delims = [], &blk)
    @segments_by_name = {}
    @field_total = 0
    @is_child = false

    setup_delimiters delims

    @elements = elements_from_segment(raw_segment)

    return unless block_given?

    callctx = eval("self", blk.binding, __FILE__, __LINE__)
    def callctx.__seg__(val = nil)
      @__seg_val__ ||= val
    end
    callctx.__seg__(self)
    # TODO: find out if this pollutes the calling namespace permanently...

    eval(METHOD_MISSING_FOR_INITIALIZER, blk.binding)
    yield self
    eval("class << self; remove_method :method_missing;end", blk.binding, __FILE__, __LINE__)
  end

  # Breaks the raw segment into elements
  # raw_segment:: is an optional String or Array which will be used as the
  #               segment's field data
  def elements_from_segment(raw_segment)
    if raw_segment.is_a? Array
      elements = raw_segment
    else
      elements = HL7::MessageParser.split_by_delimiter(raw_segment,
        @element_delim)
      if raw_segment == ""
        elements[0] = self.class.to_s.split("::").last
        elements << ""
      end
    end
    elements
  end

  def to_info
    format("%s: empty segment >> %s", self.class.to_s, @elements.inspect)
  end

  # output the HL7 spec version of the segment
  def to_s
    @elements.join(@element_delim)
  end

  # at the segment level there is no difference between to_s and to_hl7
  alias_method :to_hl7, :to_s

  # handle the e<number> field accessor
  # and any aliases that didn't get added to the system automatically
  def method_missing(sym, *args, &blk)
    base_str = sym.to_s.delete("=")
    base_sym = base_str.to_sym

    if self.class.fields.include?(base_sym)
      # base_sym is ok, let's move on
    elsif /e([0-9]+)/ =~ base_str
      # base_sym should actually be $1, since we're going by
      # element id number
      base_sym = $1.to_i
    else
      super
    end

    if sym.to_s.include?("=")
      write_field(base_sym, args)
    elsif args.length.positive?
      write_field(base_sym, args.flatten.select {|arg| arg })
    else
      read_field(base_sym)
    end
  end

  # sort-compare two Segments, 0 indicates equality
  def <=>(other)
    return nil unless other.is_a?(HL7::Message::Segment)

    # per Comparable docs: http://www.ruby-doc.org/core/classes/Comparable.html
    diff = weight - other.weight
    return -1 if diff.positive?
    return 1 if diff.negative?

    0
  end

  # get the defined sort-weight of this segment class
  # an alias for self.weight
  def weight
    self.class.weight
  end

  # return true if the segment has a parent
  def is_child_segment?
    (@is_child_segment ||= false)
  end

  # indicate whether or not the segment has a parent
  def is_child_segment=(val)
    @is_child_segment = val
  end

  # yield each element in the segment
  def each # :yields: element
    return unless @elements

    @elements.each { |e| yield e }
  end

  # get the length of the segment (number of fields it contains)
  def length
    0 unless @elements
    @elements.length
  end

  def has_children?
    respond_to?(:children)
  end

private

  def self.singleton # :nodoc:
    class << self; self end
  end

  def setup_delimiters(delims)
    delims = [delims].flatten

    @element_delim = delims.length.positive? ? delims[0] : "|"
    @item_delim = delims.length > 1 ? delims[1] : "^"
  end

  # DSL element to define a segment's sort weight
  # returns the segment's current weight by default
  # segments are sorted ascending
  def self.weight(new_weight = nil)
    if new_weight
      singleton.module_eval do
        @my_weight = new_weight
      end
    end

    singleton.module_eval do
      return 999 unless @my_weight

      @my_weight
    end
  end

  def self.convert_to_ts(value) # :nodoc:
    if value.is_a?(Time) || value.is_a?(DateTime)
      hl7_formatted_timestamp(value)
    elsif value.is_a?(Date)
      hl7_formatted_date(value)
    else
      value
    end
  end

  def self.sanitize_admin_sex!(value)
    raise HL7::InvalidDataError, "bad administrative sex value (not F|M|O|U|A|N|C)" unless /^[FMOUANC]$/.match(value) || value.nil? || value == ""

    value ||= ""
    value
  end
end