require 'matrix'
require 'spliner/spliner_section'

module Spliner
  # Spliner::Spliner provides cubic spline interpolation based on provided 
  # key points on a X-Y curve.
  #
  # == Example
  #
  #    require 'spliner'
  #    
  #    # Initialize a spline interpolation with x range 0.0..2.0
  #    my_spline = Spliner::Spliner.new [0.0, 1.0, 2.0], [0.0, 1.0, 0.5]
  #    
  #    # Interpolate for a single value
  #    y1 = my_spline[0.5]
  #    
  #    # Perform interpolation on 11 values ranging from 0..2.0
  #    y_values = my_spline[(0.0..2.0).step(0.1)]
  #
  #    # You may prefer to use the shortcut class method
  #    y2 = Spliner::Spliner[[0.0, 1.0, 2.0], [0.0, 1.0, 0.5], 0.5]
  #
  #
  # Algorithm based on http://en.wikipedia.org/wiki/Spline_interpolation
  #
  class Spliner
    attr_reader :range

    # Creates a new Spliner::Spliner object to interpolate between
    # the supplied key points. 
    #
    # The key points shoul be in increaing X order. When duplicate X 
    # values are encountered, the spline is split into two or more 
    # discontinuous sections.
    #
    # The extrapolation method may be :linear by default, using a linear 
    # extrapolation at the curve ends using the curve derivative at the 
    # end points. The :hold method will use the Y value at the nearest end 
    # point of the curve.
    #
    # @overload initialize(key_points, options)
    #   @param key_points [Hash{Float => Float}] keys are X values in increasing order, values Y
    #   @param options [Hash]
    #   @option options [Range,String] :extrapolate ('0%') either a range or percentage, eg '10.0%', or float 0.1
    #   @option options [Symbol] :emethod (:linear) extrapolation method
    #   @option options [Symbol] :fix_invalid_x (false) delete data points not in increasing order
    #
    # @overload initialize(x, y, options)
    #   @param x [Array(Float),Vector] the X values of the key points
    #   @param y [Array(Float),Vector] the Y values of the key points
    #   @param options [Hash]
    #   @option options [Range,String] :extrapolate ('0%') either a range or percentage, eg '10.0%', or float 0.1
    #   @option options [Symbol] :emethod (:linear) extrapolation method
    #   @option options [Symbol] :fix_invalid_x (false) delete data points not in increasing order
    #
    def initialize(*param)
      # sort parameters from two alternative initializer signatures
      x, y = nil
      case param.first
      when Array, Vector
        xx,yy, options = param
        x = xx.to_a
        y = yy.to_a
      else
        points, options = param
        x = points.keys
        y = points.values
      end
      options ||= {}

      if options[:fix_invalid_x]
        begin
          size_at_start = x.size
          pp = Hash[x.zip y]
          to_delete = pp.keys.each_cons(2).select {|a,b| b < a}.map(&:last)
          to_delete.each {|k| pp.delete k }
          x = pp.keys
          y = pp.values
        end while x.size < size_at_start
      end

      @sections = split_at_duplicates(x).map {|slice| SplinerSection.new x[slice], y[slice] }

      # Handle extrapolation option parameter
      options[:extrapolate].tap do |ex|
        case ex
        when /^\d+(\.\d+)?\s?%$/
          percentage = ex[/\d+(\.\d+)?/].to_f
          span = x.last - x.first
          extra = span * percentage * 0.01
          @range = (x.first - extra)..(x.last + extra)
        when Range
          @range = ex
        when Float
          span = x.last - x.first
          extra = span * ex
          @range = (x.first - extra)..(x.last + extra)
        when nil
          @range = x.first..x.last
        else
          raise 'Unable to use extrapolation parameter'
        end
      end
      @extrapolation_method = options[:emethod] || :linear
    end

    # shortcut method to instanciate a Spliner::Spliner object and
    # return a series of interpolated values. Options are like 
    # Spliner::Spliner#initialize
    #
    # @overload interpolate(points, x, options)
    #   @param points [Hash{Float => Float}] keys are X values in increasing order, values Y
    #   @param x [Float,Vector,Enumerable(Float)] X value(s) to interpolate on
    #   @param options [Hash]
    #
    # @overload interpolate(key_x, key_y, x, options)
    #   @param key_x [Array(Float),Vector] the X values of the key points
    #   @param_key_y [Array(Float),Vector] the Y values of the key points
    #   @param x [Float,Vector,Enumerable(Float)] X value(s) to interpolate on
    #   @param options [Hash]
    #
    def self.interpolate(*args)
      if (args.first.class == Hash)
        key_points, x, options = args
        s = Spliner.new key_points, (options || {})
        s[x]
      else
        key_x, key_y, x, options = args
        s = Spliner.new key_x, key_y, (options || {})
        s[x]
      end
    end

    class << self
      alias :'[]' :interpolate
    end

    # returns the ranges at each slice between duplicate X values
    def split_at_duplicates(x)
      # find all indices with duplicate x values
      dups = x.each_cons(2).map{|a,b| a== b}.each_with_index.select {|b,i| b }.map {|b,i| i}
      ([-1] + dups + [x.size - 1]).each_cons(2).map {|end0, end1| (end0 + 1)..end1 }
    end
    private :split_at_duplicates


    # returns the interpolated Y value(s) at the specified X
    #
    # @param x [Float,Vector,Enumerable(Float)] x
    #
    # == Example
    #
    #    my_spline = Spliner::Spliner.new [0.0, 1.0, 2.0], [0.0, 1.0, 0.5]
    #    # get one value
    #    y1 = my_spline.get 0.5
    #    # get many values
    #    y2 = my_spline.get [0.5, 1.5, 2.5]
    #    y3 = my_spline.get 0.5, 1.5, 2.5
    #    # get a range of values
    #    y4 = my_spline.get 1..3
    #    # generate an enumeration of x values
    #    y5 = my_spline.get (1.5..2.5).step(0.5)
    #
    def get(*x)
      xx = if x.size == 1
             x.first
           else
             x
           end

      get_func = lambda do |v|
        i = @sections.find_index {|section| section.range.cover? v }
        if i
          @sections[i].get v
        elsif range.cover? v
          extrapolate(v)
        else
          nil
        end
      end

      case xx
      when Vector
         xx.collect {|x| get_func.call(x) }
      when Enumerable
        xx.map {|x| get_func.call(x) }
      else
        get_func.call(xx)
      end
    end

    alias :'[]' :get 

    # The number of non-continuous sections used
    def sections
      @sections.size
    end



    def extrapolate(v)
      x, y, k = if v < first_x
                  [@sections.first.x.first, @sections.first.y.first, @sections.first.k.first]
                else
                  [@sections.last.x.last, @sections.last.y.last, @sections.last.k[-1]]
                end

      case @extrapolation_method
      when :hold
        y
      else
        y + k * (v - x)
      end
    end
    private :extrapolate

    def first_x
      @sections.first.x.first
    end
    private :first_x
  end
end