# encoding: utf-8

require_relative '../../hocon/config_syntax'
require_relative '../../hocon/impl'
require_relative '../../hocon/impl/config_node_complex_value'
require_relative '../../hocon/impl/config_node_field'
require_relative '../../hocon/impl/config_node_single_token'
require_relative '../../hocon/impl/tokens'

class Hocon::Impl::ConfigNodeObject
  include Hocon::Impl::ConfigNodeComplexValue

  ConfigSyntax = Hocon::ConfigSyntax
  Tokens = Hocon::Impl::Tokens

  def new_node(nodes)
    self.class.new(nodes)
  end

  def has_value(desired_path)
    @children.each do |node|
      if node.is_a?(Hocon::Impl::ConfigNodeField)
        field = node
        key = field.path.value
        if key == desired_path || key.starts_with(desired_path)
          return true
        elsif desired_path.starts_with(key)
          if field.value.is_a?(self.class)
            obj = field.value
            remaining_path = desired_path.sub_path_to_end(key.length)
            if obj.has_value(remaining_path)
              return true
            end
          end
        end
      end
    end
    false
  end

  def change_value_on_path(desired_path, value, flavor)
    children_copy = @children.clone
    seen_non_matching = false
    # Copy the value so we can change it to null but not modify the original parameter
    value_copy = value
    i = children_copy.size
    while i >= 0 do
      if children_copy[i].is_a?(Hocon::Impl::ConfigNodeSingleToken)
        t = children_copy[i].token
        # Ensure that, when we are removing settings in JSON, we don't end up with a trailing comma
        if flavor.equal?(ConfigSyntax::JSON) && !seen_non_matching && t.equal?(Tokens::COMMA)
          children_copy.delete_at(i)
        end
        i -= 1
        next
      elsif !children_copy[i].is_a?(Hocon::Impl::ConfigNodeField)
        i -= 1
        next
      end
      node = children_copy[i]
      key = node.path.value

      # Delete all multi-element paths that start with the desired path, since technically they are duplicates
      if (value_copy.nil? && key == desired_path) || (key.starts_with(desired_path) && !(key == desired_path))
        children_copy.delete_at(i)
        # Remove any whitespace or commas after the deleted setting
        j = i
        while j < children_copy.size
          if children_copy[j].is_a?(Hocon::Impl::ConfigNodeSingleToken)
            t = children_copy[j].token
            if Tokens.ignored_whitespace?(t) || t.equal?(Tokens::COMMA)
              children_copy.delete_at(j)
              j -= 1
            else
              break
            end
          else
            break
          end
          j += 1
        end
      elsif key == desired_path
        seen_non_matching = true
        before = i - 1 > 0 ? children_copy[i - 1] : nil
        if value.is_a?(Hocon::Impl::ConfigNodeComplexValue) && before.is_a?(Hocon::Impl::ConfigNodeSingleToken) &&
            Tokens.ignored_whitespace?(before.token)
          indented_value = value.indent_text(before)
        else
          indented_value = value
        end
        children_copy[i] = node.replace_value(indented_value)
        value_copy = nil
      elsif desired_path.starts_with(key)
        seen_non_matching = true
        if node.value.is_a?(self.class)
          remaining_path = desired_path.sub_path_to_end(key.length)
          children_copy[i] = node.replace_value(node.value.change_value_on_path(remaining_path, value_copy, flavor))
          if !value_copy.nil? && !(node == @children[i])
            value_copy = nil
          end
        end
      else
        seen_non_matching = true
      end
      i -= 1
    end
    self.class.new(children_copy)
  end

  def set_value_on_path(desired_path, value, flavor = ConfigSyntax::CONF)
    path = Hocon::Impl::PathParser.parse_path_node(desired_path, flavor)
    set_value_on_path_node(path, value, flavor)
  end

  def set_value_on_path_node(desired_path, value, flavor)
    node = change_value_on_path(desired_path.value, value, flavor)

    # If the desired Path did not exist, add it
    unless node.has_value(desired_path.value)
      return node.add_value_on_path(desired_path, value, flavor)
    end
    node
  end

  def indentation
    seen_new_line = false
    indentation = []

    if @children.empty?
      return indentation
    end

    @children.each_index do |i|
      unless seen_new_line
        if @children[i].is_a?(Hocon::Impl::ConfigNodeSingleToken) && Tokens.newline?(@children[i].token)
          seen_new_line = true
          indentation.push(Hocon::Impl::ConfigNodeSingleToken.new(Tokens.new_line(nil)))
        end
      else
        if @children[i].is_a?(Hocon::Impl::ConfigNodeSingleToken) &&
            Tokens.ignored_whitespace?(@children[i].token) &&
            i + 1 < @children.size &&
            (@children[i + 1].is_a?(Hocon::Impl::ConfigNodeField) || @children[i + 1].is_a?(Hocon::Impl::ConfigNodeInclude))
          # Return the indentation of the first setting on its own line
          indentation.push(@children[i])
          return indentation
        end
      end
    end
    if indentation.empty?
      indentation.push(Hocon::Impl::ConfigNodeSingleToken.new(Tokens.new_ignored_whitespace(nil, " ")))
      return indentation
    else
      # Calculate the indentation of the ending curly-brace to get the indentation of the root object
      last = @children[-1]
      if last.is_a?(Hocon::Impl::ConfigNodeSingleToken) && last.token.equal?(Tokens::CLOSE_CURLY)
        beforeLast = @children[-2]
        indent = ""
        if beforeLast.is_a?(Hocon::Impl::ConfigNodeSingleToken) &&
            Tokens.ignored_whitespace?(beforeLast.token)
          indent = beforeLast.token.token_text
        end
        indent += "  "
        indentation.push(Hocon::Impl::ConfigNodeSingleToken.new(Tokens.new_ignored_whitespace(nil, indent)))
        return indentation
      end
    end

    # The object has no curly braces and is at the root level, so don't indent
    indentation
  end

  def add_value_on_path(desired_path, value, flavor)
    path = desired_path.value
    children_copy = @children.clone
    indentation = indentation().clone

    # If the value we're inserting is a complex value, we'll need to indent it for insertion
    if value.is_a?(Hocon::Impl::ConfigNodeComplexValue) && indentation.length > 0
      indented_value = value.indent_text(indentation[-1])
    else
      indented_value = value
    end
    same_line = !(indentation.length > 0 && indentation[0].is_a?(Hocon::Impl::ConfigNodeSingleToken) &&
                    Tokens.newline?(indentation[0].token))

    # If the path is of length greater than one, see if the value needs to be added further down
    if path.length > 1
       (0..@children.size - 1).reverse_each do |i|
        unless @children[i].is_a?(Hocon::Impl::ConfigNodeField)
          next
        end
        node = @children[i]
        key = node.path.value
        if path.starts_with(key) && node.value.is_a?(self.class)
          remaining_path = desired_path.sub_path(key.length)
          new_value = node.value
          children_copy[i] = node.replace_value(new_value.add_value_on_path(remaining_path, value, flavor))
          return self.class.new(children_copy)
        end
      end
    end

    # Otherwise, construct the new setting
    starts_with_brace = @children[0].is_a?(Hocon::Impl::ConfigNodeSingleToken) && @children[0].token.equal?(Tokens::OPEN_CURLY)
    new_nodes = []
    new_nodes += indentation
    new_nodes.push(desired_path.first)
    new_nodes.push(Hocon::Impl::ConfigNodeSingleToken.new(Tokens::COLON))
    new_nodes.push(Hocon::Impl::ConfigNodeSingleToken.new(Tokens.new_ignored_whitespace(nil, ' ')))

    if path.length == 1
      new_nodes.push(indented_value)
    else
      # If the path is of length greater than one add the required new objects along the path
      new_object_nodes = []
      new_object_nodes.push(Hocon::Impl::ConfigNodeSingleToken.new(Tokens::OPEN_CURLY))
      if indentation.empty?
        new_object_nodes.push(Hocon::Impl::ConfigNodeSingleToken.new(Tokens.new_line(nil)))
      end
      new_object_nodes += indentation
      new_object_nodes.push(Hocon::Impl::ConfigNodeSingleToken.new(Tokens::CLOSE_CURLY))
      new_object = self.class.new(new_object_nodes)
      new_nodes.push(new_object.add_value_on_path(desired_path.sub_path(1), indented_value, flavor))
    end

    # Combine these two cases so that we only have to iterate once
    if flavor.equal?(ConfigSyntax::JSON) || starts_with_brace || same_line
      i = children_copy.size - 1
      while i >= 0

        # If we are in JSON or are adding a setting on the same line, we need to add a comma to the
        # last setting
        if (flavor.equal?(ConfigSyntax::JSON) || same_line) && children_copy[i].is_a?(Hocon::Impl::ConfigNodeField)
          if i + 1 >= children_copy.size ||
              !(children_copy[i + 1].is_a?(Hocon::Impl::ConfigNodeSingleToken) && children_copy[i + 1].token.equal?(Tokens::COMMA))
            children_copy.insert(i + 1, Hocon::Impl::ConfigNodeSingleToken.new(Tokens::COMMA))
            break
          end
        end

        # Add the value into the copy of the children map, keeping any whitespace/newlines
        # before the close curly brace
        if starts_with_brace && children_copy[i].is_a?(Hocon::Impl::ConfigNodeSingleToken) &&
            children_copy[i].token == Tokens::CLOSE_CURLY
          previous = children_copy[i - 1]
          if previous.is_a?(Hocon::Impl::ConfigNodeSingleToken) && Tokens.newline?(previous.token)
            children_copy.insert(i - 1, Hocon::Impl::ConfigNodeField.new(new_nodes))
            i -= 1
          elsif previous.is_a?(Hocon::Impl::ConfigNodeSingleToken) && Tokens.ignored_whitespace?(previous.token)
            before_previous = children_copy[i - 2]
            if same_line
              children_copy.insert(i - 1, Hocon::Impl::ConfigNodeField.new(new_nodes))
              i -= 1
            elsif before_previous.is_a?(Hocon::Impl::ConfigNodeSingleToken) && Tokens.newline?(before_previous.token)
              children_copy.insert(i - 2, Hocon::Impl::ConfigNodeField.new(new_nodes))
              i -= 2
            else
              children_copy.insert(i, Hocon::Impl::ConfigNodeField.new(new_nodes))
            end
          else
            children_copy.insert(i, Hocon::Impl::ConfigNodeField.new(new_nodes))
          end
        end

        i -= 1
      end
    end
    unless starts_with_brace
      if children_copy[-1].is_a?(Hocon::Impl::ConfigNodeSingleToken) && Tokens.newline?(children_copy[-1].token)
        children_copy.insert(-2, Hocon::Impl::ConfigNodeField.new(new_nodes))
      else
        children_copy.push(Hocon::Impl::ConfigNodeField.new(new_nodes))
      end
    end
    self.class.new(children_copy)
  end

  def remove_value_on_path(desired_path, flavor)
    path = Hocon::Impl::PathParser.parse_path_node(desired_path, flavor).value
    change_value_on_path(path, nil, flavor)
  end
end
