# encoding: ascii-8bit

# Copyright 2014 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt

require 'cosmos/packets/packet_config'
require 'cosmos/packets/packet_item'

module Cosmos

  class PacketItemParser
    # @param parser [ConfigParser] Configuration parser
    # @param packet [Packet] The packet the item should be added to
    # @param cmd_or_tlm [String] Whether this is :bn
    def self.parse(parser, packet, cmd_or_tlm)
      parser = PacketItemParser.new(parser)
      parser.verify_parameters(cmd_or_tlm)
      parser.create_packet_item(packet, cmd_or_tlm)
    end

    # @param parser [ConfigParser] Configuration parser
    def initialize(parser)
      @parser = parser
      @usage = get_usage()
    end

    def verify_parameters(cmd_or_tlm)
      if @parser.keyword.include?('ITEM') && cmd_or_tlm == PacketConfig::COMMAND
        raise @parser.error("ITEM types are only valid with TELEMETRY", @usage)
      elsif @parser.keyword.include?('PARAMETER') && cmd_or_tlm == PacketConfig::TELEMETRY
        raise @parser.error("PARAMETER types are only valid with COMMAND", @usage)
      end
      # The usage is formatted with brackets <XXX> around each option so
      # count the number of open brackets to determine the number of options
      max_options = @usage.count("<")
      # The last two options (description and endianness) are optional
      @parser.verify_num_parameters(max_options-2, max_options, @usage)
    end

    def create_packet_item(packet, cmd_or_tlm)
      item = PacketItem.new(@parser.parameters[0],
                            get_bit_offset(),
                            get_bit_size(),
                            get_data_type(),
                            get_endianness(packet),
                            get_array_size(),
                            :ERROR) # overflow
      if cmd_or_tlm == PacketConfig::COMMAND
        item.range = get_range()
        item.default = get_default()
      end
      item.id_value = get_id_value()
      item.description = get_description()
      if append?
        item = packet.append(item)
      else
        item = packet.define(item)
      end
      item
    rescue => err
      raise @parser.error(err, @usage)
    end

    private

    def append?
      @parser.keyword.include?("APPEND")
    end

    def get_data_type
      index = append? ? 2 : 3
      @parser.parameters[index].upcase.to_sym
    end

    def get_bit_offset
      return 0 if append?
      Integer(@parser.parameters[1])
    rescue => err # In case Integer fails
      raise @parser.error(err, @usage)
    end

    def get_bit_size
      index = append? ? 1 : 2
      Integer(@parser.parameters[index])
    rescue => err
      raise @parser.error(err, @usage)
    end

    def get_array_size
      return nil unless (@parser.keyword.include?('ARRAY'))
      index = append? ? 3 : 4
      Integer(@parser.parameters[index])
    rescue => err
      raise @parser.error(err, @usage)
    end

    def get_endianness(packet)
      params = @parser.parameters
      max_options = @usage.count("<")
      if params[max_options-1]
        endianness = params[max_options-1].to_s.upcase.intern
        if endianness != :BIG_ENDIAN and endianness != :LITTLE_ENDIAN
          raise @parser.error("Invalid endianness #{endianness}. Must be BIG_ENDIAN or LITTLE_ENDIAN.", @usage)
        end
      else
        endianness = packet.default_endianness
      end
      endianness
    end

    def get_range
      return nil if @parser.keyword.include?('ARRAY')
      data_type = get_data_type()
      return nil if data_type == :STRING or data_type == :BLOCK

      index = append? ? 3 : 4
      min = ConfigParser.handle_defined_constants(
        @parser.parameters[index].convert_to_value, get_data_type(), get_bit_size())
      max = ConfigParser.handle_defined_constants(
        @parser.parameters[index+1].convert_to_value, get_data_type(), get_bit_size())
      min..max
    end

    def get_default
      return [] if @parser.keyword.include?('ARRAY')

      index = append? ? 3 : 4
      data_type = get_data_type()
      if data_type == :STRING or data_type == :BLOCK        
        # If the default value is 0x<data> (no quotes), it is treated as
        # binary data.  Otherwise, the default value is considered to be a string.
        if (@parser.parameters[index].upcase.start_with?("0X")         and
            !@parser.line.include?("\"#{@parser.parameters[index]}\"") and
            !@parser.line.include?("\'#{@parser.parameters[index]}\'"))
          return @parser.parameters[index].hex_to_byte_string
        else
          return @parser.parameters[index]
        end
      else
        return ConfigParser.handle_defined_constants(
          @parser.parameters[index+2].convert_to_value, get_data_type(), get_bit_size())
      end
    end

    def get_id_value
      return nil unless @parser.keyword.include?('ID_')
      data_type = get_data_type
      if @parser.keyword.include?('ITEM')
        index = append? ? 3 : 4
      else # PARAMETER
        index = append? ? 5 : 6
        # STRING and BLOCK PARAMETERS don't have min and max values
        index -= 2 if data_type == :STRING || data_type == :BLOCK
      end
      if data_type == :DERIVED
        raise @parser.error("DERIVED data type not allowed for Identifier")
      end
      @parser.parameters[index]
    end

    def get_description
      max_options = @usage.count("<")
      @parser.parameters[max_options-2] if @parser.parameters[max_options-2]
    end

    # There are many different usages of the ITEM and PARAMETER keywords so
    # parse the keyword and parameters to generate the correct usage information.
    def get_usage
      keyword = @parser.keyword
      params = @parser.parameters
      usage = "#{keyword} <ITEM NAME> "
      usage << "<BIT OFFSET> " unless keyword.include?("APPEND")
      usage << bit_size_usage()
      usage << type_usage()
      usage << "<TOTAL ARRAY BIT SIZE> " if keyword.include?("ARRAY")
      usage << id_usage()
      usage << "<DESCRIPTION (Optional)> <ENDIANNESS (Optional)>"
      usage
    end

    def bit_size_usage
      if @parser.keyword.include?("ARRAY")
        "<ARRAY ITEM BIT SIZE> "
      else
        "<BIT SIZE> "
      end
    end

    def type_usage
      keyword = @parser.keyword
      # Item type usage is simple so just return it
      return "<TYPE: INT/UINT/FLOAT/STRING/BLOCK/DERIVED> " if keyword.include?("ITEM")

      # Build up the parameter type usage based on the keyword
      usage = "<TYPE: "
      params = @parser.parameters
      # ARRAY types don't have min or max or default values
      if keyword.include?("ARRAY")
        usage << "INT/UINT/FLOAT/STRING/BLOCK> "
      else
        begin
          data_type = get_data_type()
        rescue
          # If the data type could not be determined set something
          data_type == :INT
        end
        # STRING and BLOCK types do not have min or max values
        if data_type == :STRING || data_type == :BLOCK
          usage << "STRING/BLOCK> "
        else
          usage << "INT/UINT/FLOAT> <MIN VALUE> <MAX VALUE> "
        end
        # ID Values do not have default values
        usage << "<DEFAULT_VALUE> " unless keyword.include?("ID")
      end
      usage
    end

    def id_usage
      return '' unless @parser.keyword.include?("ID")
      if @parser.keyword.include?("PARAMETER")
        "<DEFAULT AND ID VALUE> "
      else
        "<ID VALUE> "
      end
    end

  end
end # module Cosmos