# Copyright (C) 2011 AMEE UK Ltd. - http://www.amee.com
# Released as Open Source Software under the BSD 3-Clause license. See LICENSE.txt for details.
# 
# :title: Module: AMEE::DataAbstraction::TermsListAnalyticsSupport

module AMEE
  module Analytics

    # Mixin module for the <i>AMEE::DataAbstraction::Term</i> class, providing
    # methods for handling collections of calculations.
    #
    module TermsListAnalyticsSupport
      
      def name
        first.name if analogous?
      end

      # Returns <tt>true</tt> if all terms within the list have the same label.
      # Otherwise, returns <tt>false</tt>.
      # 
      # This enables a check as to whether all terms represent the same thing,
      # i.e. same calculation component (i.e. the same drill choice, or profile
      # item value, or return value, or metadata type).
      #
      def analogous?
        labels.uniq.size == (1 or nil)
      end

      # Returns <tt>true</tt> if all terms within the list have the same label
      # AND contain consistent units. Otherwise, returns <tt>false</tt>.
      # 
      # This enables a term list to be manipulated numerically, for example, by
      # producing a sum or a mean across all terms.
      #
      def homogeneous?
        analogous? and homogeneous_units? and homogeneous_per_units?
      end

      # Returns <tt>true</tt> if TermsList is NOT homogeneous, i.e. it does NOT
      # contain all analogous terms with corresponding units. Otherwise, returns
      # <tt>false</tt>.
      #
      def heterogeneous?
        !homogeneous?
      end

      # Returns <tt>true</tt> if all terms within the list are represented by the
      # same unit or are all <tt>nil</tt>. Otherwise, returns <tt>false</tt>.
      #
      def homogeneous_units?
        return true if all? { |term| term.unit.nil? } or
          ( all? { |term| term.unit.is_a? Quantity::Unit::Base } and
            map { |term| term.unit.label }.uniq.size == 1 )
        return false
      end

      # Returns <tt>true</tt> if all terms within the list are represented by the
      # same PER unit or are all <tt>nil</tt>. Otherwise, returns <tt>false</tt>.
      #
      def homogeneous_per_units?
        return true if all? { |term| term.per_unit.nil? } or
          ( all? { |term| term.per_unit.is_a? Quantity::Unit::Base } and
            map { |term| term.per_unit.label }.uniq.size == 1 )
        return false
      end

      # Returns the label which defines all terms in contained within <tt>self</tt>,
      # if they are all the same. Otherwise, returns <tt>nil</tt>.
      #
      def label
        first.label if analogous?
      end

      def +(other_list)
        self.class.new(self.to_a + other_list.to_a)
      end

      def -(other_list)
        other_list = [other_list].flatten
        self.delete_if { |term| other_list.include?(term) }
      end

      def first_of_each_type
        labels = self.labels.uniq
        terms = labels.map {|label| find { |term| term.label == label } }
        AMEE::DataAbstraction::TermsList.new(terms)
      end

      # Returns the label of the unit which is predominantly used across all terms
      # in the list, e.g.
      #
      #  list.predominant_unit      #=> kg
      #
      #  list.predominant_unit      #=> kWh
      #
      # Returns nil if all units are blank
      #
      def predominant_unit
        terms = reject { |term| term.unit.nil? }
        unit = terms.group_by { |term| term.unit.label }.
          max {|a,b| a.last.size <=> b.last.size }.first unless terms.blank?
        return unit
      end

      # Returns the label of the per unit which is predominantly used across all terms
      # in the list, e.g.
      #
      #  list.predominant_per_unit      #=> h
      #
      #  list.predominant_per_unit      #=> kWh
      #
      # Returns nil if all per units are blank
      #
      def predominant_per_unit
        terms = reject { |term| term.per_unit.nil? }
        unit = terms.group_by { |term| term.per_unit.label }.
          max {|a,b| a.last.size <=> b.last.size }.first unless terms.blank?
        return unit
      end

      # Returns <tt>true</tt> if all terms in the list have numeric values.
      # Otherwise, returns <tt>false</tt>.
      #
      def all_numeric?
        all? { |term| term.has_numeric_value? }
      end

      # Returns a new instance of <i>TermsList</i> comprising only those terms
      # belongong to <tt>self</tt> which have numeric values.
      #
      # This is useful for establishing which terms in a list to perform numerical
      # operations on
      #
      def numeric_terms
        AMEE::DataAbstraction::TermsList.new select { |term| term.has_numeric_value? }
      end

      # Returns a new instance of <i>TermsList</i> with all units standardized and
      # the respective term values adjusted accordingly.
      # 
      # The unit and per units to be standardized to can be specified as the first
      # and second arguments respectively. Either the unit name, symbol or label
      # (as defined in the <i>Quantify</i> gem) can be used. If no arguments are
      # specified, the standardized units represent those which are predominant
      # in the list, e.g.
      #
      #   list.standardize_units                  #=> <TermsList>
      #
      #   list.standardize_units(:t,:kWh)         #=> <TermsList>
      #
      #   list.standardize_units('pound')         #=> <TermsList>
      #
      #   list.standardize_units(nil, 'BTU')      #=> <TermsList>
      #
      def standardize_units(unit=nil,per_unit=nil)
        return self if homogeneous? && ((unit.nil? or (first.unit && first.unit.label == unit)) &&
           (per_unit.nil? || (first.per_unit && first.per_unit.label == per_unit)))
        unit = predominant_unit if unit.nil?
        per_unit = predominant_per_unit if per_unit.nil?
        new_terms = map { |term| term.convert_unit(:unit => unit, :per_unit => per_unit) }
        AMEE::DataAbstraction::TermsList.new new_terms
      end

      # Returns a new instance of <i>Result</i> which represents the sum of all
      # term values within the list.
      #
      # Any terms within self which contain non-numeric values are ignored.
      #
      # If the terms within <tt>self</tt> do not contain consistent units, they
      # are standardized by default to the unit (and per unit) which predominate
      # in the list. Alternatively, the required unit and per units can be
      # specified as arguments using the same conventions as the
      # <tt>#standardize_units</tt> method.
      #
      def sum(unit=nil,per_unit=nil)
        unit = predominant_unit if unit.nil?
        per_unit = predominant_per_unit if per_unit.nil?
        value = numeric_terms.standardize_units(unit,per_unit).inject(0.0) do |sum,term|
          sum + term.value
        end
        template = self
        Result.new { label template.label; value value; unit unit; per_unit per_unit; name template.name }
      end

      # Returns a new instance of <i>Result</i> which represents the mean of all
      # term values within the list.
      #
      # Any terms within self which contain non-numeric values are ignored.
      #
      # If the terms within <tt>self</tt> do not contain consistent units, they
      # are standardized by default to the unit (and per unit) which predominate
      # in the list. Alternatively, the required unit and per units can be
      # specified as arguments using the same conventions as the
      # <tt>#standardize_units</tt> method.
      #
      def mean(unit=nil,per_unit=nil)
        list = numeric_terms
        sum = list.sum(unit,per_unit)
        Result.new { label sum.label; value (sum.value/list.size); unit sum.unit; per_unit sum.per_unit; name sum.name }
      end

      # Returns a representation of the term with most prevalent value in
      # <tt>self</tt>, i.e. the modal value. This method considers both numerical
      # and text values.
      #
      # If only a single modal value is discovered an instance of the class
      # <i>Result</i> is returning representing the modal value. Where multiple
      # modal values occur a new instance of <i>TermsList</i> is returned
      # containing <i>Result</i> representations of each modal value.
      #
      def mode
        groups = standardize_units.reject { |term| term.value.nil? }.
          group_by { |term| term.value }.map(&:last)
        max_group_size = groups.max {|a,b| a.size <=> b.size }.size
        max_groups = groups.select {|a| a.size == max_group_size}
        if max_groups.size == 1
          max_groups.first.first.to_result
        else
          AMEE::DataAbstraction::TermsList.new max_groups.map { |group| group.first.to_result }
        end
      end

      # Returns a representation of the term with median value in <tt>self</tt>.
      # This method considers both numerical and text values.
      #
      # If <tt>self</tt> has an even-numbered size, the median is caluclated as
      # the mean of the values of the two centrally placed terms (having been
      # sorted according to their value attributes).
      #
      def median
        new_list = standardize_units
        midpoint = new_list.size/2
        if new_list.size % 2.0 == 1
          median_term = new_list.sort_by_value[midpoint]
        elsif new_list.size % 2.0 == 0
          median_term = new_list.sort_by_value[midpoint-1, 2].mean
        else
          raise
        end
        median_term.to_result
      end

      # Convenience method for initializing instances of the <i>Result</i> class.
      # Intialize the new object with the attributes described by <tt>label</tt>,
      # <tt>value</tt>, <tt>unit</tt> and <tt>per_unit</tt>. The unit and per_unit
      # attributes default to <tt>nil</tt> if left unspecified.
      #
      def initialize_result(label,value,unit=nil,per_unit=nil)
        Result.new { label label; value value; unit unit; per_unit per_unit }
      end

      # Move an individual term to a specified location (index) within the list.
      # The specific term is selected on the basis of one of it's attributes values,
      # with the attribute to use (e.g. :value, :unit) given by <tt>attr</attr>
      # and value by <tt>value</tt>. The location within the list to move the term
      # is given as an index integer value as the final argument.
      #
      def move_by(attr,value,index)
        if attr == :unit || attr == :per_unit
          value = Unit.for value
        end
        term = find {|t| t.send(attr) == value }
        return if term.nil?
        delete(term)
        insert(index, term)
      end

      # Rotate the list terms by one element - shifts the first-placed term to the
      # end of the list, advancing all terms forward by one place.
      def rotate
        push(self.shift)
      end

      # Sorts the terms list in place according to the term attribute indicated by
      # <tt>attr</tt>, returning <tt>self</tt>.
      #
      # If differences in units exist between terms, sorting occur based on the
      # absolute quantities implied.
      #
      #   my_terms_list.sort_by! :value
      #
      #                   #=> <AMEE::DataAbstraction::TermsList ... >
      #
      def sort_by!(attr)
        replace(sort_by(attr))
      end

      # Similar to <tt>#sort_by!</tt> but returns a new instance of
      # <i>TermsList</i> arranged according to the values on the
      # attribute <tt>attr</tt>.
      #
      #
      # If differences in units exist between terms, sorting occur based on the
      # absolute quantities implied.
      #
      # E.g.
      #
      #   my_terms_list.sort_by :value
      #
      #                   #=> <AMEE::DataAbstraction::TermsList ... >
      #
      def sort_by(attr)
        # 1. Remove unset terms before sort and append at end
        #
        # 2. Establish set terms
        #
        # 3. Zip together with corresponding standardized units list creating a
        # list of Term pairs
        #
        # 4. Sort list according to standardized Terms
        #
        # 5. Return map of original (now sorted) Terms

        unset_terms, set_terms = self.partition { |term| term.unset? || term.value.nil? }
        standardized_set_terms = AMEE::DataAbstraction::TermsList.new(set_terms).standardize_units
        ordered_set_terms = set_terms.zip(standardized_set_terms).sort! do |term,other_term|
          term[1].send(attr) <=> other_term[1].send(attr)
        end.map {|term_array| term_array[0]}
        AMEE::DataAbstraction::TermsList.new(ordered_set_terms + unset_terms)
      end

      # Return an instance of <i>TermsList</i> containing only terms labelled
      # :type.
      #
      # This method overrides the standard #type method (which is deprecated) and
      # mimics the functionality provied by the first #method_missing method in
      # dynamically retrieving a subset of terms according their labels.
      #
      def type
        AMEE::DataAbstraction::TermsList.new select{ |x| x.label == :type }
      end

      def respond_to?(method)
        if labels.include? method.to_sym
          return true
        elsif method.to_s =~ /sort_by_(.*)!/ and self.class::TermProperties.include? $1.to_sym
          return true
        elsif method.to_s =~ /sort_by_(.*)/ and self.class::TermProperties.include? $1.to_sym
          return true
        else
          super
        end
      end

      # Syntactic sugar for several instance methods.
      #
      # ---
      #
      # Call a method on <tt>self</tt> which named after a specific term label
      # contained within <tt>self</tt> and return a new instance of the
      # <tt>TermsList</tt> class containing each of those terms. E.g.,
      #
      #   my_terms = my_terms_list.type              #=> <AMEE::DataAbstraction::TermsList>
      #   my_terms.label                             #=> :type
      #
      #   my_terms = my_terms_list.mass              #=> <AMEE::DataAbstraction::TermsList>
      #   my_terms.label                             #=> :mass
      #
      #   my_terms = my_terms_list.co2               #=> <AMEE::DataAbstraction::TermsList>
      #   my_terms.label                             #=> :co2
      #
      # ---
      #
      # Call either the <tt>#sort_by</tt> or <tt>#sort_by!</tt> methods including
      # the argument term as part of the method name, e.g.,
      #
      #   my_calculation_collection.sort_by_value
      #
      #                   #=> <AMEE::DataAbstraction::TermsList ... >
      #
      #   my_calculation_collection.sort_by_name!
      #
      #                   #=> <AMEE::DataAbstraction::TermsList ... >
      #
      def method_missing(method, *args, &block)
        if labels.include? method
          AMEE::DataAbstraction::TermsList.new select{ |x| x.label == method }
        elsif method.to_s =~ /sort_by_(.*)!/ and self.class::TermProperties.include? $1.to_sym
          sort_by! $1.to_sym
        elsif method.to_s =~ /sort_by_(.*)/ and self.class::TermProperties.include? $1.to_sym
          sort_by $1.to_sym
        else
          super
        end
      end

    end
  end
end