##
# Contains many class methods which perform useful functions 
module Digiproc::Functions

    extend Digiproc::Convolvable::ClassMethods, Digiproc::FourierTransformable::GenericMethods, Digiproc::DataProperties

    ##
    # Performs cross correlation cacluatlino for two arrays of numerics
    def self.cross_correlation(data1, data2)
        self.conv(data1, data2.reverse)
    end

    ##
    # Maps an array of numerics to a range defined by a min and max
    # Output an array of samples mapped to within the bounds of the min and max
    # == Input args
    # samples:: Array of Numerics
    # min:: Float, the minimum desired value of the new mapped data
    # max:: Float, the maximum desired value of the new mapped data
    def self.map_data_to_range(samples, min, max)
        target_center = (max + min) / 2.0
        target_range = max.to_f - min
        smax, smin = samples.max, samples.min
        sample_center = (smax + smin) / 2.0
        sample_range = smax.to_f - smin
        center_map = target_center - sample_center 
        range_map = target_range / sample_range
        mapping = ->(n){ (n + center_map) * range_map }
        process(samples, mapping)
    end

    ##
    # Return a lambda equation which will map a dataset within a starting min and max to a 
    # new dataset with a different min and max.
    # == Input Args
    # starting_min:: Float, minimum val of data to be mapped
    # starting_max:: Float, maximum val of data to be mapped
    # target_min:: Float, minimum value of data after mapping
    # target_max:: Float, maximum value of data after mapping
    def self.map_to_eqn(starting_min, starting_max, target_min, target_max)
        target_center = center_of(target_max, target_min)
        target_range = range_of(target_max, target_min)
        starting_center = center_of(starting_max, starting_min) 
        starting_range = range_of(starting_max.to_f, starting_min)
        range_map = target_range.to_f / starting_range
        ->(n){ (n - starting_center) * range_map + target_center}
    end

    ##
    # == Input args
    # *args:: Can be a variable number of Floats, an array, or a hash with key value pairs of point locations with the value of probability values
    # If a variable number of floats are an array of floats are given, they will be treated as points on a 1-dimentional line and the return will
    # be an array of numbers which are the sampe points centered around the origin .
    # If a hash is given, the keys will be assumed to be places on the 1D line and the values will be the corresponding probabilities of those points.
    # The same calculation will be carried out, except taking inot account the appropriate weighting.
    # Note: Arguents are X-dimensional ponins as array => optional hash value for probability
    def self.translate_center_to_origin(*args)
        args = args[0] if args.length == 1 and args.first.is_a? Enumerable
        points = []
        probabilities = []
        dimensions = 0
        if args.is_a? Hash 
            args.each do |k,v|
                points << k
                probabilities << v
            end
        else
            points = args
            probabilities = points.map{ 1.0 / points.length }
        end
    
        if probabilities.include?(nil)
            probabilities.map{ 1.0 / points.length }
        end
        dimensions = points.first.length
        weighted_points = points.map.with_index do |point, index|
            point.map{|dimension| -1 * dimension * probabilities[index]}
        end
        a = weighted_points.reduce(Array.new(dimensions,0), :plus)
        points.map{ |vector| vector.plus(a) }
    end

    ##
    # Return the center of a max and min point
    # == Input args
    # max:: float
    # min:: float
    def self.center_of(max, min)
        (max + min) / 2.0
    end

    ##
    # Return the range of a max and min point
    # == Input args
    # max:: float
    # min:: float
    def self.range_of(max, min)
        max.to_f - min
    end

    ##
    # Return an arary of zeros of length num
    # == Input args
    # num:: Integer
    def self.zeros(num)
        Array.new(num, 0)
    end
    
    ##
    # Return an array of ones of length numn
    # == Input args
    # num:: Integer
    def self.ones(num)
        Array.new(num, 1)
    end
    
    ##
    # Similar to linspace in numpy, return an array of numbers given a min, max, and total number 
    # of elements
    # == Input args
    # start:: flaot, the minimum number and index 0 of the output array
    # stop:: float, the maximum number and last index of the output array
    # number:: integer, the number of elements in the output array
    def self.linspace(start, stop, number)
        rng = 0...number
        interval = (stop - start).to_f / (number - 1)
        rng.map{ |val| start + interval * val }
    end

    ##
    # Given an array of numerics, if it is monitonic (ie flat, increaseing, or decreasing), return
    # a symbol representing its state. Return nil of the data is not monotonic
    # == Input args
    # data:: Array[Numeric]
    def self.monotonic_state(data)
        last_value = data.first
        state = [:flat, :increasing, :decreasing]
        state_index = 0
        for i in 1...data.length do
            return state[state_index] if state[state_index].nil? 
            slope = data[i] - last_value
            if slope > 0
                state_index = 1 if state[:state_index] == :flat
                return nil if state[:state_index] != :increasing
            elsif slope < 0
                state_index = 2 if state[:state_index] == :flat
                return nil if state[:state_index] != :decreasing
            end
            last_value = data[i]
        end
        return state[:state_index]
    end

    ##
    # Return true or false based on if the data is monotoic (ie has a consistant slope direction)
    # ==Input Arg
    # data:: Array[Numeric]
    def self.monotonic?(data)
        !!monotonic_state(data)
    end

    ##
    # Transform a binary number represented in string form into an integer
    # == Input Arg
    # bin_str:: String
    ## Digiproc::Functions.bin_str_to_i("101101") # => 45
    def self.bin_str_to_i(bin_str)
        str_arr = bin_str.split("").reverse
        sum = 0
        str_arr.each_with_index do |digit, index|
            sum += digit.to_i * 2 ** index
        end
        sum
    end

    ##
    # XOR two strings representing binary numbers, return the XOR of them in decimal
    ## Digiproc::Functions.str_xor("1011","1001") # => 2
    def self.str_xor(str1, str2)
        bin_str_to_i(str1) ^ bin_str_to_i(str2)
    end

    

    @fact_memo = {}

    ##
    # Given a number, return its factorial
    def self.fact(n)
        raise ArgumentError.new("n must be positive") if n < 0
        return 1 if n <= 1
        return @fact_memo[n] if not @fact_memo[n].nil?
        x = n * fact(n - 1)
        @fact_memo[n] = x
        return x
    end

    # def self.populate_large_factorial_memoization
    #     for i in 1..10 do 
    #         fact(10000 * i)
    #     end 
    # end

    ##
    # Return the value of sinc(x), given an input x (float)
    # sinc = sin(x) / x
    def self.sinc(x)
        return 1 if x == 0
        Math.sin(x) / x.to_f
    end 
    
    ##
    # Maps values using the lambda equation
    # == Input args
    # values:: Collection of values
    # eqn:: lambda 
    def self.process(values, eqn)
        values.map{ |val| eqn.call(val) }
    end

end