# encoding: utf-8

require 'stringio'
require_relative '../../hocon/impl'
require_relative '../../hocon/config_syntax'
require_relative '../../hocon/impl/tokenizer'
require_relative '../../hocon/impl/config_node_path'
require_relative '../../hocon/impl/tokens'
require_relative '../../hocon/config_value_type'
require_relative '../../hocon/config_error'

class Hocon::Impl::PathParser
  ConfigSyntax = Hocon::ConfigSyntax
  SimpleConfigOrigin = Hocon::Impl::SimpleConfigOrigin
  Tokenizer = Hocon::Impl::Tokenizer
  Tokens = Hocon::Impl::Tokens
  ConfigNodePath = Hocon::Impl::ConfigNodePath
  ConfigValueType = Hocon::ConfigValueType
  ConfigBadPathError = Hocon::ConfigError::ConfigBadPathError



  class Element
    def initialize(initial, can_be_empty)
      @can_be_empty = can_be_empty
      @sb = StringIO.new(initial)
    end

    attr_accessor :can_be_empty, :sb

    def to_string
      "Element(#{@sb.string},#{@can_be_empty})"
    end
  end

  def self.api_origin
    SimpleConfigOrigin.new_simple("path parameter")
  end

  def self.parse_path_node(path, flavor = ConfigSyntax::CONF)
    reader = StringIO.new(path)

    begin
      tokens = Tokenizer.tokenize(api_origin, reader,
                                  flavor)
      tokens.next # drop START
      parse_path_node_expression(tokens, api_origin, path, flavor)
    ensure
      reader.close
    end
  end

  def self.parse_path(path)
    speculated = speculative_fast_parse_path(path)
    if not speculated.nil?
      return speculated
    end

    reader = StringIO.new(path)

    begin
      tokens = Tokenizer.tokenize(api_origin, reader, ConfigSyntax::CONF)
      tokens.next # drop START
      return parse_path_expression(tokens, api_origin, path)
    ensure
      reader.close
    end
  end

  def self.parse_path_node_expression(expression, origin, original_text = nil,
                                      flavor = ConfigSyntax::CONF)
    path_tokens = []
    path = parse_path_expression(expression, origin, original_text, path_tokens, flavor)
    ConfigNodePath.new(path, path_tokens);
  end

  def self.parse_path_expression(expression, origin, original_text = nil,
                                  path_tokens = nil, flavor = ConfigSyntax::CONF)
    # each builder in "buf" is an element in the path
    buf = []
    buf.push(Element.new("", false))

    if !expression.has_next?
      raise ConfigBadPathError.new(
                origin,
                original_text,
                "Expecting a field name or path here, but got nothing")
    end

    while expression.has_next?
      t = expression.next

      if ! path_tokens.nil?
        path_tokens << t
      end

      # Ignore all IgnoredWhitespace tokens
      next if Tokens.ignored_whitespace?(t)

      if Tokens.value_with_type?(t, ConfigValueType::STRING)
        v = Tokens.value(t)
        # this is a quoted string; so any periods
        # in here don't count as path separators
        s = v.transform_to_string
        add_path_text(buf, true, s)
      elsif t == Tokens::EOF
        # ignore this; when parsing a file, it should not happen
        # since we're parsing a token list rather than the main
        # token iterator, and when parsing a path expression from the
        # API, it's expected to have an EOF.
      else
        # any periods outside of a quoted string count as
        # separators
        text = nil
        if Tokens.value?(t)
          # appending a number here may add
          # a period, but we _do_ count those as path
          # separators, because we basically want
          # "foo 3.0bar" to parse as a string even
          # though there's a number in it. The fact that
          # we tokenize non-string values is largely an
          # implementation detail.
          v = Tokens.value(t)

          # We need to split the tokens on a . so that we can get sub-paths but still preserve
          # the original path text when doing an insertion
          if ! path_tokens.nil?
            path_tokens.delete_at(path_tokens.size - 1)
            path_tokens.concat(split_token_on_period(t, flavor))
          end
          text = v.transform_to_string
        elsif Tokens.unquoted_text?(t)
          # We need to split the tokens on a . so that we can get sub-paths but still preserve
          # the original path text when doing an insertion on ConfigNodeObjects
          if ! path_tokens.nil?
            path_tokens.delete_at(path_tokens.size - 1)
            path_tokens.concat(split_token_on_period(t, flavor))
          end
          text = Tokens.unquoted_text(t)
        else
          raise ConfigBadPathError.new(
                    origin,
                    original_text,
                    "Token not allowed in path expression: #{t}" +
                        " (you can double-quote this token if you really want it here)")
        end

        add_path_text(buf, false, text)
      end
    end

    pb = Hocon::Impl::PathBuilder.new
    buf.each do |e|
      if (e.sb.length == 0) && !e.can_be_empty
        raise Hocon::ConfigError::ConfigBadPathError.new(
                  origin,
                  original_text,
                  "path has a leading, trailing, or two adjacent period '.' (use quoted \"\" empty string if you want an empty element)")
      else
        pb.append_key(e.sb.string)
      end
    end

    pb.result
  end

  def self.split_token_on_period(t, flavor)
    token_text = t.token_text
    if token_text == "."
      return [t]
    end
    split_token = token_text.split('.')
    split_tokens = []
    split_token.each do |s|
      if flavor == ConfigSyntax::CONF
        split_tokens << Tokens.new_unquoted_text(t.origin, s)
      else
        split_tokens << Tokens.new_string(t.origin, s, "\"#{s}\"")
      end
      split_tokens << Tokens.new_unquoted_text(t.origin, ".")
    end
    if token_text[-1] != "."
      split_tokens.delete_at(split_tokens.size - 1)
    end
    split_tokens
  end

  def self.add_path_text(buf, was_quoted, new_text)
    i = if was_quoted
          -1
        else
          new_text.index('.') || -1
        end
    current = buf.last
    if i < 0
      # add to current path element
      current.sb << new_text
      # any empty quoted string means this element can
      # now be empty.
      if was_quoted && (current.sb.length == 0)
        current.can_be_empty = true
      end
    else
      # "buf" plus up to the period is an element
      current.sb << new_text[0, i]
      # then start a new element
      buf.push(Element.new("", false))
      # recurse to consume remainder of new_text
      add_path_text(buf, false, new_text[i + 1, new_text.length - 1])
    end
  end

  # the idea is to see if the string has any chars or features
  # that might require the full parser to deal with.
  def self.looks_unsafe_for_fast_parser(s)
    last_was_dot = true # // start of path is also a "dot"
    len = s.length
    if s.empty?
      return true
    end
    if s[0] == "."
      return true
    end
    if s[len - 1] == "."
      return true
    end

    (0..len).each do |i|
      c = s[i]
      if c =~ /^\w$/
        last_was_dot = false
        next
      elsif c == '.'
        if last_was_dot
          return true # ".." means we need to throw an error
        end
        last_was_dot = true
      elsif c == '-'
        if last_was_dot
          return true
        end
        next
      else
        return true
      end
    end

    if last_was_dot
      return true
    end

    false
  end

  def self.fast_path_build(tail, s, path_end)
    # rindex takes last index it should look at, end - 1 not end
    split_at = s.rindex(".", path_end - 1)
    tokens = []
    tokens << Tokens.new_unquoted_text(nil, s)
    # this works even if split_at is -1; then we start the substring at 0
    with_one_more_element = Path.new(s[split_at + 1..path_end], tail)
    if split_at < 0
      with_one_more_element
    else
      fast_path_build(with_one_more_element, s, split_at)
    end
  end

  # do something much faster than the full parser if
  # we just have something like "foo" or "foo.bar"
  def self.speculative_fast_parse_path(path)
    s = Hocon::Impl::ConfigImplUtil.unicode_trim(path)
    if looks_unsafe_for_fast_parser(s)
      return nil
    end

    fast_path_build(nil, s, s.length)
  end

end
