#!/usr/bin/env ruby
#
# Copyright 2011-2013, Dell
# Copyright 2013-2014, SUSE LINUX Products GmbH
#
# 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 'rubygems'
require 'json'
require 'getoptlong'

MAGIC_DOT = '@@ESCAPED_DOT@@'

#
# Parsing options can be added by adding to this list before calling opt_parse
#
@options = [
    [ [ '--help', '-h', GetoptLong::NO_ARGUMENT ], "--help or -h - help" ],
    [ [ '--attribute', '-a', GetoptLong::REQUIRED_ARGUMENT ], "--attribute <attribute> or -a <attribute> - Name of attribute to set. Use . as sub-attribute delimiter" ],
    [ [ '--value', '-v', GetoptLong::REQUIRED_ARGUMENT ], "--value <value> or -v <value> - Value to set for attribute" ],
    [ [ '--raw', '-r', GetoptLong::NO_ARGUMENT ], "--raw or -r - value is a raw argument already in JSON format" ],
    [ [ '--no-clobber', '-n', GetoptLong::NO_ARGUMENT ], "--no-clobber - Don't overwrite existing attributes" ],
    [ [ '--remove', GetoptLong::NO_ARGUMENT ], "--remove - Remove attribute instead of setting it" ],
]

@attribute = nil
@value = nil
@raw = false
@no_clobber = false
@remove = false

def usage (rc)
  puts "Usage: json-edit [options] <filename>"
  @options.each do |options|
    puts "  #{options[1]}"
  end
  puts "  Use '-' for <filename> to use stdin/stdout for json input/ouput."
  exit rc
end

def help
  usage 0
end


### Start MAIN ###

def opt_parse()
  sub_options = @options.map { |x| x[0] }
  opts = GetoptLong.new(*sub_options)

  opts.each do |opt, arg|
    case opt
      when '--help'
        usage 0
      when '--attribute'
        if @attribute.nil?
          @attribute = arg
        else
          usage -1
        end
      when '--value'
        if @value.nil?
          @value = arg
        else
          usage -1
        end
      when '--raw'
        @raw = true
      when '--no-clobber'
        @no_clobber = true
      when '--remove'
        @remove = true
      else
        usage -1
    end
  end

  if ARGV.length > 1
    usage -1
  end
end

def main()
  opt_parse

  begin
    if ARGV.length != 1
      usage -1
    end

    filename = ARGV[0]
    if filename == '-'
      json = JSON.parse(STDIN.read())
    elsif File.exists?(filename)
      json = JSON.parse(File.open(filename).read())
    else
      json = JSON.parse("{}")
    end

    if @raw
      real_value = JSON.parse("{\"value\": #{@value}}")["value"]
    else
      real_value = @value
    end

    # Protect already escaped periods from being interpreted as
    # element delimiters.  Notice that the 1st parameter to gsub is a
    # String not a Regexp.
    elements = @attribute.gsub('\.', MAGIC_DOT).split('.')
    elements.map! {|element| element.gsub(MAGIC_DOT, '.')}
    data = json

    parent_attribute = nil
    current_attribute = nil

    elements[0, elements.length - 1].each { |element|
      parent_attribute = current_attribute
      current_attribute = parent_attribute.nil? ? element : "#{parent_attribute}.#{element.gsub('.', '\.')}"

      if data.is_a?(Hash)
        unless data.has_key?(element) && (data[element].is_a?(Hash) || data[element].is_a?(Array))
          if @remove
            exit 0
          end
          if data.has_key?(element) && @no_clobber
            puts "Not overwriting #{current_attribute}"
            exit 0
          end
          data[element] = {}
        end

        data = data[element]
      elsif data.is_a?(Array)
        index = element.to_i

        is_integer = index.to_s == element
        raise "invalid index for array #{parent_attribute}: #{element}" if not is_integer

        if index > data.length
          if @remove
            exit 0
          elsif index > data.length
            raise "array #{parent_attribute} too small for index: #{element}"
          end
        end

        if data.length == index
          data << {}
        end

        unless data[index].is_a?(Hash) || data[index].is_a?(Array)
          if @remove
            exit 0
          end
          if @no_clobber
            puts "Not overwriting #{current_attribute}"
            exit 0
          end
          data[index] = {}
        end

        data = data[index]
      else
        raise "#{parent_attribute} is not an array nor a hash"
      end
    }

    last_element = elements[-1]
    parent_attribute = current_attribute

    if data.is_a?(Hash)
      unless @remove
        if @no_clobber and data.has_key?(last_element)
          puts "Not overwriting #{@attribute}"
          exit 0
        else
          data[last_element] = real_value
        end
      else
        if data.has_key?(last_element)
          data.delete(last_element)
        else
          exit 0
        end
      end
    elsif data.is_a?(Array)
      index = last_element.to_i

      is_integer = index.to_s == last_element
      raise "invalid index for array #{parent_attribute}: #{last_element}" if not is_integer

      if @remove
        if index < data.length
          data.delete_at(index)
        else
          exit 0
        end
      else
        raise "array #{parent_attribute} too small for index: #{last_element}" unless index <= data.length

        if @no_clobber and index < data.length
          puts "Not overwriting #{@attribute}"
          exit 0
        end

        if index == data.length
          data << real_value
        else
          data[index] = real_value
        end
      end
    else
      raise "#{parent_attribute} is not an array nor a hash"
    end

    if filename == '-'
      puts JSON.pretty_generate(json)
    else
      File.open(filename, 'w') {|f| f.write(JSON.pretty_generate(json)) }
    end
  rescue => ex
    puts "json-edit failed: #{ex.message}"
    ex.backtrace.each { |bt_line| puts "#{bt_line}" }
    exit -1
  end

end

main
