#--
# Author:: Daniel DeLeo (<dan@chef.io>)
# Copyright:: Copyright 2011-2016, Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative "text_formatter"

class Chef
  class Knife
    module Core

      # Allows includer knife commands to  return multiple attributes
      # @brief knife node show NAME -a ATTR1 -a ATTR2
      module MultiAttributeReturnOption
        # @private
        def self.included(includer)
          includer.class_eval do
            option :field_separator,
              short: "-S SEPARATOR",
              long: "--field-separator SEPARATOR",
              description: "Character separator used to delineate nesting in --attribute filters (default \".\")"

            option :attribute,
              short: "-a ATTR1 [-a ATTR2]",
              long: "--attribute ATTR1 [--attribute ATTR2] ",
              description: "Show one or more attributes",
              proc: Proc.new { |a|
                Chef::Config[:knife][:attribute] ||= []
                Chef::Config[:knife][:attribute].push(a)
              }
          end
        end
      end

      # The base presenter class for displaying structured data in knife commands.
      # This is not an abstract base class, and it is suitable for displaying
      # most kinds of objects that knife needs to display.
      class GenericPresenter

        attr_reader :ui
        attr_reader :config

        # Instaniates a new GenericPresenter. This is generally handled by the
        # Chef::Knife::UI object, though you need to match the signature of this
        # method if you intend to use your own presenter instead.
        def initialize(ui, config)
          @ui, @config = ui, config
        end

        # Is the selected output format a data interchange format?
        # Returns true if the selected output format is json or yaml, false
        # otherwise. Knife search uses this to adjust its data output so as not
        # to produce invalid JSON output.
        def interchange?
          case parse_format_option
          when :json, :yaml
            true
          else
            false
          end
        end

        # Returns a String representation of +data+ that is suitable for output
        # to a terminal or perhaps for data interchange with another program.
        # The representation of the +data+ depends on the value of the
        # `config[:format]` setting.
        def format(data)
          case parse_format_option
          when :summary
            summarize(data)
          when :text
            text_format(data)
          when :json
            Chef::JSONCompat.to_json_pretty(data)
          when :yaml
            require "yaml"
            YAML.dump(data)
          when :pp
            require "stringio" unless defined?(StringIO)
            # If you were looking for some attribute and there is only one match
            # just dump the attribute value
            if config[:attribute] && data.length == 1
              data.values[0]
            else
              out = StringIO.new
              PP.pp(data, out)
              out.string
            end
          end
        end

        # Converts the user-supplied value of `config[:format]` to a Symbol
        # representing the desired output format.
        # ===Returns
        # returns one of :summary, :text, :json, :yaml, or :pp
        # ===Raises
        # Raises an ArgumentError if the desired output format could not be
        # determined from the value of `config[:format]`
        def parse_format_option
          case config[:format]
          when "summary", /^s/, nil
            :summary
          when "text", /^t/
            :text
          when "json", /^j/
            :json
          when "yaml", /^y/
            :yaml
          when "pp", /^p/
            :pp
          else
            raise ArgumentError, "Unknown output format #{config[:format]}"
          end
        end

        # Summarize the data. Defaults to text format output,
        # which may not be very summary-like
        def summarize(data)
          text_format(data)
        end

        # Converts the +data+ to a String in the text format. Uses
        # Chef::Knife::Core::TextFormatter
        def text_format(data)
          TextFormatter.new(data, ui).formatted_data
        end

        def format_list_for_display(list)
          config[:with_uri] ? list : list.keys.sort { |a, b| a <=> b }
        end

        def format_for_display(data)
          if formatting_subset_of_data?
            format_data_subset_for_display(data)
          elsif config[:id_only]
            name_or_id_for(data)
          elsif config[:environment] && data.respond_to?(:chef_environment)
            { "chef_environment" => data.chef_environment }
          else
            data
          end
        end

        def format_data_subset_for_display(data)
          subset = if config[:attribute]
                     result = {}
                     Array(config[:attribute]).each do |nested_value_spec|
                       nested_value = extract_nested_value(data, nested_value_spec)
                       result[nested_value_spec] = nested_value
                     end
                     result
                   elsif config[:run_list]
                     run_list = data.run_list.run_list
                     { "run_list" => run_list }
                   else
                     raise ArgumentError, "format_data_subset_for_display requires attribute, run_list, or id_only config option to be set"
                   end
          { name_or_id_for(data) => subset }
        end

        def name_or_id_for(data)
          data.respond_to?(:name) ? data.name : data["id"]
        end

        def formatting_subset_of_data?
          config[:attribute] || config[:run_list]
        end

        # GenericPresenter is used in contexts where MultiAttributeReturnOption
        # is not, so we need to set the default value here rather than as part
        # of the CLI option.
        def attribute_field_separator
          config[:field_separator] || "."
        end

        def extract_nested_value(data, nested_value_spec)
          nested_value_spec.split(attribute_field_separator).each do |attr|
            data =
              if data.is_a?(Array)
                data[attr.to_i]
              elsif data.respond_to?(:[], false) && data.respond_to?(:key?) && data.key?(attr)
                data[attr]
              elsif data.respond_to?(attr.to_sym, false)
                # handles -a chef_environment and other things that hang of the node and aren't really attributes
                data.public_send(attr.to_sym)
              else
                nil
              end
          end
          # necessary (?) for coercing objects (the run_list object?) to hashes
          ( !data.is_a?(Array) && data.respond_to?(:to_hash) ) ? data.to_hash : data
        end

        def format_cookbook_list_for_display(item)
          if config[:with_uri]
            item.inject({}) do |collected, (cookbook, versions)|
              collected[cookbook] = {}
              versions["versions"].each do |ver|
                collected[cookbook][ver["version"]] = ver["url"]
              end
              collected
            end
          else
            versions_by_cookbook = item.inject({}) do |collected, ( cookbook, versions )|
              collected[cookbook] = versions["versions"].map { |v| v["version"] }
              collected
            end
            key_length = versions_by_cookbook.empty? ? 0 : versions_by_cookbook.keys.map(&:size).max + 2
            versions_by_cookbook.sort.map do |cookbook, versions|
              "#{cookbook.ljust(key_length)} #{versions.join("  ")}"
            end
          end
        end

      end
    end
  end
end
