require "ipsumizer/version"

class Ipsumizer
  attr_reader :prefix, :sentencer

  DEFAULT_SENTENCER = Regexp.new %r{([.!?][.!?\p{Final_Punctuation}\p{Close_Punctuation}"\s]*)}

  DEFAULT_PREFIX = 4

  def initialize( sentences, prefix: DEFAULT_PREFIX, sentencer: DEFAULT_SENTENCER )
    @prefix = prefix.to_i
    fail "prefix length must be non-negative: #{prefix}" unless prefix > 0
    @sentencer = sentencer
    @transitions = {}
    if sentences.is_a? String
      sentences = sentence sentences
    end
    sentences = sentences.map{ |s| s.strip.gsub /\s/, ' ' }.select{ |s| s =~ /\S/ }
    fail "no sentences" unless sentences.any?
    sentences.each do |s|
      key = ''
      i = 0
      (0..s.length).each do |i|
        nxt = if i == s.length
          nil
        else
          s[i]
        end
        counts = @transitions[key] ||= {}
        counts[nxt] = counts[nxt].to_i + 1
        if nxt
          key += nxt
          if key.length > prefix
            key = key[1..-1]
          end
        end
      end
    end
    @transitions.each do |pfx, counts|
      total = counts.values.reduce(:+)
      probabilities = counts.values.map{ |n| n.to_r / total }
      @transitions[pfx] = AliasTable.new( counts.keys, probabilities )
    end
  end

  # split a text into sentences, where sentences are separated by on of '.', '!', and '?', optionally preceded by
  # double quotes or closing brackets and so forth
  def sentence(text)
    text.split(sentencer).each_slice(2).map{ |*bits| bits.join }.select{ |s| s =~ /\S/ }
  end

  # make a random sentence
  def speak
    pfx = s = ''
    loop do
      nxt = @transitions[pfx]&.generate
      break unless nxt
      s += nxt
      pfx += nxt
      if pfx.length > prefix
        pfx = pfx[1..-1]
      end
    end
    s
  end

  # copied in here and fixed because the gem was in a broken state
  class AliasTable
    def initialize(x_set, p_value)
      @p_primary = p_value.map(&:to_r)
      @x = x_set.clone.freeze
      @alias = Array.new(@x.length)
      parity = Rational(1, @x.length)
      group = @p_primary.each_index.group_by { |i| @p_primary[i] <=> parity }
      parity_set = group.fetch(0, [])
      parity_set.each { |i| @p_primary[i] = Rational(1) }
      deficit_set = group.fetch(-1, [])
      surplus_set = group.fetch(1, [])
      until deficit_set.empty?
        deficit = deficit_set.pop
        surplus = surplus_set.pop
        @p_primary[surplus] -= parity - @p_primary[deficit]
        @p_primary[deficit] /= parity
        @alias[deficit] = @x[surplus]
        if @p_primary[surplus] == parity
          @p_primary[surplus] = Rational(1)
        else
          (@p_primary[surplus] < parity ? deficit_set : surplus_set) << surplus
        end
      end
    end

    def generate
      column = rand(@x.length)
      rand <= @p_primary[column] ? @x[column] : @alias[column]
    end
  end

end