require "parslet"

class ErlangParser < Parslet::Parser
  root :outermost
  # only designed for rabbitmq config files for now:
  rule(:outermost) { filler? >> array.maybe >> dot.maybe }

  rule(:exp) do
    (tuple | array | binary | string | bool | identifier | float | integer) >> filler?
  end

  rule(:array) do
    str("[") >> filler? >> (
      exp.repeat(1) >>
      (comma >> exp).repeat
    ).maybe.as(:array) >> str("]") >> filler?
  end

  rule(:tuple) do
    str("{") >> filler? >> (
      exp.repeat(1) >> filler? >>
      (comma >> exp).repeat
    ).maybe.as(:tuple) >> str("}") >> filler?
  end

  rule(:filler?) { space.repeat }
  rule(:space)   { match('\s+') | match["\n"] | comment }

  rule(:comment) { str("%") >> (match["\n\r"].absent? >> any).repeat }
  rule(:comma) { str(",") >> filler? }
  rule(:dot) { str(".") >> filler? }
  rule(:bool) { str("true").as(:bool) | str("false").as(:bool) }

  rule(:identifier) do
    (match("[a-zA-Z]") >> match("[a-zA-Z0-9_]").repeat).as(:identifier) >> filler?
  end

  rule(:float) do
    (
      integer >> (
        str(".") >> match("[0-9]").repeat(1) |
        str("e") >> match("[0-9]").repeat(1)
      ).as(:e)
    ).as(:float) >> filler?
  end

  rule(:integer) do
    ((str("+") | str("-")).maybe >> match("[0-9]").repeat(1)).as(:integer) >> filler?
  end

  rule(:string) { stringS | stringD }

  rule(:stringS) do
    str("'") >> (
      str("\\") >> any | str("'").absent? >> any
    ).repeat.as(:string) >> str("'") >> filler?
  end

  rule(:stringD) do
    str('"') >> (
      str("\\") >> any | str('"').absent? >> any
    ).repeat.as(:string) >> str('"') >> filler?
  end

  rule(:binary_item) do
    (string | integer) >>
      (str(":") >> integer).maybe.as(:size) >>
      (str("/") >> identifier).maybe.as(:type) >>
      filler?
  end

  rule(:binary) do
    str("<<") >> filler? >> (
      binary_item.repeat(1) >>
      (comma >> binary_item).repeat
    ).maybe.as(:binary) >> str(">>") >> filler?
  end
end

class ErlangBitstream
  def initialize
    @data = []     # a stream of 8-bit numbers
    @cur_bits = "" # a string of binary bits 10010010...
  end

  TYPES = {
    "integer" => 8,
    "float" => 8 * 8,
    "utf8" => 8,
    "utf16" => 8 * 2,
    "utf32" => 8 * 4,
  }.freeze

  def bit_size(size, type)
    raise "Cannot specify size and type at the same time." if !type.nil? && !size.nil?
    return (size || 8).to_i if type.nil?

    TYPES[type] || raise("Cannot handle binary-stream type #{type}")
  end

  def add(i)
    if i[:integer].nil? && i[:string].nil?
      raise "No data provided, internal error for binary-stream processing!"
    end

    s = bit_size(i[:size], i[:type])
    unless i[:string].nil?
      str2int(i[:string].to_s, i[:type]).map { |e| add_bits(int2bits(e, 8)) }
    else
      add_int(i[:integer], s)
    end
  rescue RuntimeError => e
    raise "Error processing Erlang bit string "\
          "'#{i[:string] || i[:integer]}:#{i[:size]}/#{i[:type]}'. #{e.message}"
  end

  def str2int(s, type)
    case type
    when "utf8" then s.encode("utf-8").unpack("C*")
    when "utf16" then s.encode("utf-16").unpack("C*").drop(2)
    when "utf32" then s.encode("utf-32").unpack("C*").drop(4)
    when "integer", "float" then raise "Cannot handle bit string as type #{type}"
    else s.split("").map { |x| x.ord & 0xff }
    end
  end

  def int2bits(i, len)
    format("%0#{len}b", i)
  end

  def add_int(v, size)
    x = v.to_i & (2**size - 1) # only get the bits specified in size
    add_bits(int2bits(x, size))
  end

  def add_bits(s)
    b = (@cur_bits + s).scan(/.{1,8}/)
    @data += b[0..-2].map { |x| x.to_i(2) }
    @cur_bits = b.last
  end

  def value(encoding = "utf-8")
    # fill in the rest
    rest = "0" * (8 - @cur_bits.length) + @cur_bits
    arr = @data + [rest.to_i(2)]
    s = arr.pack("C*")
    s.force_encoding(encoding) unless encoding.nil?
    s
  end
end

class ErlangTransform < Parslet::Transform
  class Tuple < Array; end
  class Identifier < String; end

  def self.assemble_binary(seq)
    b = ErlangBitstream.new
    seq.each { |i| b.add(i) }
    b.value
  end

  rule(string: simple(:x)) { x.to_s }
  rule(string: []) { "" }
  rule(integer: simple(:x)) { x.to_i }
  rule(float: { integer: simple(:a), e: simple(:b) }) { (a + b).to_f }
  rule(bool: "true") { true }
  rule(bool: "false") { false }
  rule(binary: subtree(:x)) { x.nil? ? "" : ErlangTransform.assemble_binary(x) }
  rule(identifier: simple(:x)) { Identifier.new(x.to_s) }
  rule(array: subtree(:x)) { Array(x) }
  rule(tuple: subtree(:x)) do
    x.nil? ? Tuple.new : Tuple.new(x)
  end
end

class ErlangConfigFile
  def self.parse(content)
    lex = ErlangParser.new.parse(content)
    tree = ErlangTransform.new.apply(lex)
    turn_to_hash(tree)
  end

  def self.turn_to_hash(t)
    if t.is_a?(Array) && t.all? { |x| x.class == ErlangTransform::Tuple && x.length == 2 }
      Hash[t.map { |i| [i[0], turn_to_hash(i[1])] }]
    else
      t
    end
  end
end
