class TerraformLandscape::TerraformPlan

rubocop:disable Metrics/ClassLength
#######################################################################
explanation of the plan to the user.
This allows us to easily inspect the plan and present a more readable
Represents the parsed output of ‘terraform plan`.
#######################################################################

def attribute_indent_amount_for_resource(resource)

def attribute_indent_amount_for_resource(resource)
  longest_name_length = resource[:attributes].keys.reduce(0) do |longest, name|
    name.length > longest ? name.length : longest
  end
  longest_name_length + 8
end

def display(output)

def display(output)
  @out = output
  @ast.each do |resource|
    display_resource(resource)
    @out.newline
  end
end

def display_added_or_removed_attribute(

def display_added_or_removed_attribute(
  change_color,
  attribute_name,
  attribute_value,
  attribute_value_indent,
  attribute_value_indent_amount
)
  @out.print "    #{attribute_name}:".ljust(attribute_value_indent_amount, ' ')
                                     .colorize(change_color)
  evaluated_string = attribute_value.undump
  if json?(evaluated_string)
    @out.print to_pretty_json(evaluated_string).gsub("\n",
                                                     "\n#{attribute_value_indent}")
                                               .colorize(change_color)
  else
    @out.print "\"#{evaluated_string.colorize(change_color)}\""
  end
  @out.newline
end

def display_attribute( # rubocop:disable Metrics/ParameterLists

rubocop:disable Metrics/ParameterLists
def display_attribute( # rubocop:disable Metrics/ParameterLists
  resource,
  change_color,
  attribute_name,
  attribute_value,
  attribute_change_reason,
  attribute_value_indent_amount
)
  attribute_value_indent = ' ' * attribute_value_indent_amount
  if %i[~ -/+].include?(resource[:change])
    display_modified_attribute(change_color,
                               attribute_name,
                               attribute_value,
                               attribute_change_reason,
                               attribute_value_indent,
                               attribute_value_indent_amount)
  else
    display_added_or_removed_attribute(change_color,
                                       attribute_name,
                                       attribute_value,
                                       attribute_value_indent,
                                       attribute_value_indent_amount)
  end
end

def display_diff(old, new, indent)

def display_diff(old, new, indent)
  @out.print Diffy::Diff.new(old, new, { context: @diff_context_lines })
    .to_s(String.disable_colorization ? :text : :color)
                        .gsub("\n", "\n" + indent)
                        .strip
end

def display_modified_attribute(

rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
def display_modified_attribute(
  change_color,
  attribute_name,
  attribute_value,
  attribute_change_reason,
  attribute_value_indent,
  attribute_value_indent_amount
)
  # Since the attribute line is always of the form "old value" => "new value"
  attribute_value =~ /^ *(".*") *=> *(".*") *$/
  old = Regexp.last_match[1].undump
  new = Regexp.last_match[2].undump
  return if old == new && new != '<sensitive>' # Don't show unchanged attributes
  @out.print "    #{attribute_name}:".ljust(attribute_value_indent_amount, ' ')
                                     .colorize(change_color)
  if json?(new)
    # Value looks like JSON, so prettify it to make it more readable
    fancy_old = "#{to_pretty_json(old)}\n"
    fancy_new = "#{to_pretty_json(new)}\n"
    display_diff(fancy_old, fancy_new, attribute_value_indent)
  elsif old.include?("\n") || new.include?("\n")
    # Multiline content, so display nicer diff
    display_diff("#{old}\n", "#{new}\n", attribute_value_indent)
  else
    # Typical values, so just show before/after
    @out.print '"' + old.colorize(:red) + '"'
    @out.print ' => '.colorize(:light_black)
    @out.print '"' + new.colorize(:green) + '"'
  end
  if attribute_change_reason
    @out.print " (#{attribute_change_reason})".colorize(:magenta)
  end
  @out.newline
end

def display_resource(resource) # rubocop:disable Metrics/MethodLength

rubocop:disable Metrics/MethodLength
def display_resource(resource) # rubocop:disable Metrics/MethodLength
  change_color = CHANGE_SYMBOL_TO_COLOR[resource[:change]]
  resource_header = "#{resource[:change]} #{resource[:resource_type]}." \
                    "#{resource[:resource_name]}".colorize(change_color)
  if resource[:reason]
    resource_header += " (#{resource[:reason]})".colorize(:magenta)
  end
  if resource[:additional_reason]
    resource_header += " (#{resource[:additional_reason]})".colorize(:magenta)
  end
  @out.puts resource_header
  # Determine longest attribute name so we align all values at same indentation
  attribute_value_indent_amount = attribute_indent_amount_for_resource(resource)
  resource[:attributes].each do |attribute_name, attribute_value_and_reason|
    attribute_value = attribute_value_and_reason[:value]
    attribute_change_reason = attribute_value_and_reason[:reason]
    display_attribute(resource,
                      change_color,
                      attribute_name,
                      attribute_value,
                      attribute_change_reason,
                      attribute_value_indent_amount)
  end
end

def from_output(string)

def from_output(string)
  # Our grammar assumes output with Unix line endings
  string = string.gsub("\r\n", "\n")
  return new([]) if string.strip.empty?
  tree = parser.parse(string)
  raise ParseError, parser.failure_reason unless tree
  new(tree.to_ast)
end

def initialize(plan_ast, options = {})

Create a plan from an abstract syntax tree (AST).
def initialize(plan_ast, options = {})
  @ast = plan_ast
  @diff_context_lines = options.fetch(:diff_context_lines, DEFAULT_DIFF_CONTEXT_LINES)
end

def json?(value)

def json?(value)
  ['{', '['].include?(value.to_s[0]) &&
    (JSON.parse(value) rescue nil) # rubocop:disable Style/RescueModifier
end

def parser

def parser
  @parser ||=
    begin
      Treetop.load(GRAMMAR_FILE)
      TerraformPlanParser.new
    end
end

def recursive_sort(obj)

def recursive_sort(obj)
  case obj
  when Array
    obj.map { |item| recursive_sort(item) }
  when Hash
    obj.keys.sort.each_with_object({}) do |key, hash|
      hash[key] = recursive_sort(obj[key])
    end
  else
    obj
  end
end

def to_pretty_json(value)

def to_pretty_json(value)
  # Can't JSON.parse an empty string, so handle it separately
  return '' if value.strip.empty?
  sorted = recursive_sort(JSON.parse(value))
  JSON.pretty_generate(sorted,
                       {
                         indent: '  ',
                         space: ' ',
                         object_nl: "\n",
                         array_nl: "\n"
                       })
end