require 'cv/param'
require 'mspire/user_param'
require 'mspire/cv/param'
require 'nokogiri'
require 'andand'

module Mspire
  module CV
    module Paramable

      attr_accessor :cv_params
      attr_accessor :user_params
      attr_accessor :ref_param_groups

      def params
        cv_params + ref_param_groups.flat_map(&:params) + user_params 
      end

      def each_param(&block)
        return enum_for __method__ unless block
        cv_params.each(&block)
        ref_param_groups.flat_map(&:params).each(&block)
        user_params.each(&block)
        nil
      end

      def params?
        total_num_params = cv_params.size + 
          ref_param_groups.reduce(0) {|sum,group| sum + 
            group.params.size } + user_params.size
        total_num_params > 0
      end

      def each_accessionable_param(&block)
        return enum_for __method__ unless block
        cv_params.each(&block)
        ref_param_groups.flat_map(&:params).each(&block)
        nil
      end

      def accessionable_params
        cv_params + ref_param_groups.flat_map(&:params)
      end

      #def params_by_name
      #  params.index_by &:name
      #end

      #def params_by_accession
      #  accessionable_params.index_by &:accession
      #end
      
      # returns the value if the param exists by that name.  Returns true if
      # the param exists but has no value. returns false if no param
      def fetch(name)
        param = each_param.find {|param| param.name == name}
        if param
          param.value || true
        else
          false
        end
      end

      def fetch_by_accession(acc)
        param = accessionable_params.find {|v| v.accession == acc }
        if param
          param.value || true
        else
          false
        end
      end
      alias_method :fetch_by_acc, :fetch_by_accession

      def param?(name)
        params.any? {|param| param.name == name }
      end

      def initialize
        @cv_params = []
        @user_params = []
        @ref_param_groups = []
      end
      alias_method :params_init, :initialize

      def param_by_accession(acc)
        each_accessionable_param.find {|v| v.accession == acc }
      end
      alias_method :param_by_acc, :param_by_accession

      # takes an array of values, each of which is fed into describe!
      # returns self.
      def describe_many!(array)
        array.each do |arg|
          if arg.is_a?(Array)
            describe!(*arg)
          else
            describe!(arg)
          end
        end
        self
      end

      # reads the paramable nodes and returns self.  Use this if your element
      # does not have anything besides paramable elements.
      def describe_self_from_xml!(xml_node, ref_hash=nil)
        describe_from_xml!(xml_node, ref_hash)
        self
      end

      # takes a node with children that are cvParam, userParam or
      # referenceableParamGroupRef and a hash containing
      # referenceableParamGroup objects indexed by id.  The only time ref_hash
      # should be left nil is for the referenceableParamGroup itself.
      #
      # All param elements are required to appear before other elements, so
      # the code walks through each child node to see if it is a paramable
      # element.  The first child node that is not paramable is returned (or
      # nil if none)
      #
      # returns the next child node after the paramable elements or nil if none
      def describe_from_xml!(xml_node, ref_hash=nil)
        return nil unless (child_n = xml_node.child) 
        loop do
          array = 
            case child_n.name
            when 'referenceableParamGroupRef'
              @ref_param_groups << ref_hash[child_n[:ref]]
            when 'cvParam'
              @cv_params << Mspire::CV::Param[ child_n[:accession], child_n[:value] ]
            when 'userParam'
              @user_params << Mspire::UserParam.new(child_n[:name], child_n[:value], child_n[:type])
            else # assumes that the above precede any following children as per the spec
              break 
            end
          if (unit_acc = child_n[:unitAccession])
            array.last.unit = ::CV::Param.new(child_n[:unitCvRef], unit_acc, child_n[:unitName])
          end
          break unless child_n = child_n.next
        end
        child_n
      end

      # Expects arguments describing a single CV::Param, Mspire::UserParam, or
      # Mspire::Mzml::ReferenceableParamGroup
      #
      #     obj.describe! 'MS:1000130'  # a positive scan
      #     obj.describe! CV::Param['MS:1000130']  # same behavior
      #
      #     # base peak intensity, units=number of counts
      #     obj.describe! "MS:1000505", 1524.5865478515625, 'MS:1000131'
      #
      # returns self
      def describe!(*args)
        return self if args.first.nil?
        case (arg=args.first)
        when String
          @cv_params << Mspire::CV::Param[ *args ]
        when Mspire::Mzml::ReferenceableParamGroup
          @ref_param_groups << arg
        else
          if arg.is_a?(Mspire::UserParam)
            @user_params << arg
          else
            @cv_params << arg
          end
        end
        self
      end

      # iterates over @params and calls .to_xml on each object.
      def to_xml(xml)
        [:ref_param_groups, :cv_params, :user_params].each do |kind|
          self.send(kind).each do |obj|
            obj.to_xml(xml)
          end
        end
        xml
      end

    end
  end
end