#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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 "qpid/spec"
require 'pathname'
require 'fileutils'

module Qpid::Spec010

  include Qpid::Spec

  # XXX: workaround for ruby bug/missfeature
  Reference = Reference
  Loader = Loader

  class Spec

    ENCODINGS = {
      String => "vbin16",
      Fixnum => "int64",
      Bignum => "int64",
      Float => "float",
      NilClass => "void",
      Array => "list",
      Hash => "map"
    }

    fields(:major, :minor, :port, :children)

    def init()
      @controls = {}
      @commands = {}
      @structs = {}
      @types = {}
      children.each {|c|
        case c
        when Control
          @controls[c.code] = c
        when Command
          @commands[c.code] = c
        when Struct
          @structs[c.code] = c
        when Type
          @types[c.code] = c unless c.code.nil?
        end
      }
    end

    attr_reader :controls, :commands, :structs, :types

    def [](key)
      return @children[key]
    end

    def encoding(klass)
      if ENCODINGS.has_key?(klass)
        return self[ENCODINGS[klass]]
      end
      for base in klass.__bases__
        result = encoding(base)
        return result unless result.nil?
      end
    end

    def inspect; "spec"; end
  end

  class Constant

    fields(:name, :value)

    attr :parent, true

  end

  class Type

    fields(:name, :code, :fixed, :variable)

    attr :parent, true

    def present?(value)
      if @fixed == 0
        return value
      else
        return !value.nil?
      end
    end

    def encode(codec, value)
      codec.send("write_#{name}", value)
    end

    def decode(codec)
      return codec.send("read_#{name}")
    end

    def inspect; name; end

  end

  class Domain < Type

    fields(:name, :type, :enum)

    attr :parent, true

    def encode(codec, value)
      @type.encode(codec, value)
    end

    def decode(codec)
      return @type.decode(codec)
    end

  end

  class Enum
    fields(:choices)

    def [](choice)
      case choice
      when String
        choice = choice.to_sym
        return choices.find { |c| c.name == choice }
      when Symbol
        return choices.find { |c| c.name == choice }
      else
        return choices.find { |c| c.value == choice }
      end
    end

    def method_missing(name, *args)
      raise ArgumentError.new("wrong number of arguments") unless  args.empty?
      return self[name].value
    end

  end

  class Choice
    fields(:name, :value)
  end

  class Composite

    fields(:name, :code, :size, :pack, :fields)

    attr :parent, true

    # Python calls this 'new', but that has special meaning in Ruby
    def create(*args)
      return Qpid::struct(self, *args)
    end

    def decode(codec)
      codec.read_size(@size)
      codec.read_uint16() unless @code.nil?
      return Qpid::struct(self, self.decode_fields(codec))
    end

    def decode_fields(codec)
      flags = 0
      pack.times {|i| flags |= (codec.read_uint8() << 8*i)}

      result = {}

      fields.each_index {|i|
        f = @fields[i]
        if flags & (0x1 << i) != 0
          result[f.name] = f.type.decode(codec)
        else
          result[f.name] = nil
        end
      }

      return result
    end

    def encode(codec, value)
      sc = Qpid::StringCodec.new(@spec)
      sc.write_uint16(@code) unless @code.nil?
      encode_fields(sc, value)
      codec.write_size(@size, sc.encoded.size)
      codec.write(sc.encoded)
    end

    def encode_fields(codec, values)
      # FIXME: This could be written cleaner using select
      # instead of flags
      flags = 0
      fields.each_index do |i|
        f = fields[i]
        flags |= (0x1 << i) if f.type.present?(values[f.name])
      end

      pack.times { |i| codec.write_uint8((flags >> 8*i) & 0xFF) }

      fields.each_index do |i|
        f = fields[i]
        f.type.encode(codec, values[f.name]) if flags & (0x1 << i) != 0
      end
    end

    def inspect; name; end

  end

  class Field

    fields(:name, :type, :exceptions)

    def default()
      return nil
    end

  end

  class Struct < Composite

    def present?(value)
      return !value.nil?
    end

  end

  class Action < Composite; end

  class Control < Action

    def segment_type
      @parent[:segment_type].enum[:control].value
    end

    def track
      @parent[:track].enum[:control].value
    end

  end

  class Command < Action

    attr_accessor :payload, :result

    def segment_type
      @parent["segment_type"].enum["command"].value
    end

    def track
      @parent["track"].enum["command"].value
    end

  end

  class Doc
    fields(:type, :title, :text)
  end

  class Loader010 < Loader

    def initialize()
      super()
    end

    def klass
      cls = element
      until cls.nil?
        break if cls.name == "class"
        cls = cls.parent
      end
      return cls
    end

    def scope
      if element.name == "struct"
        return nil
      else
        return class_name
      end
    end

    def class_name
      cls = klass
      if cls.nil?
        return nil
      else
        return parse_name(cls.attributes["name"].strip)
      end
    end

    def class_code
      cls = klass
      if cls.nil?
        return 0
      else
        return parse_int(cls.attributes["code"].strip)
      end
    end

    def parse_decl(value)
      name = parse_name(value)

      s = scope
      if s.nil?
        return name
      else
        return :"#{s}_#{name}"
      end
    end

    def parse_code(value)
      c = parse_int(value)
      if c.nil?
        return nil
      else
        return c | (class_code << 8)
      end
    end

    def parse_type(value)
      name = parse_name(value.sub(".", "_"))
      cls = class_name
      return Reference.new {|spec|
        candidates = [name]
        candidates << :"#{cls}_#{name}" unless cls.nil?
        for c in candidates
          child = spec[c]
          break unless child.nil?
        end
        if child.nil?
          raise Exception.new("unresolved type: #{name}")
  else
    child
  end
}
    end

    def load_amqp()
      children = nil

      for s in ["constant", "type", "domain", "struct", "control",
                  "command"]
        ch = load(s)
        if children.nil?
          children = ch
        else
          children += ch
        end
        children += load("class/#{s}")
      end
      children += load("class/command/result/struct")
      Spec.new(attr("major", :int), attr("minor", :int), attr("port", :int),
               children)
    end

    def load_constant()
      Constant.new(attr("name", :decl), attr("value", :int))
    end

    def load_type()
      Type.new(attr("name", :decl), attr("code", :code),
               attr("fixed-width", :int), attr("variable-width", :int))
    end

    def load_domain()
      Domain.new(attr("name", :decl), attr("type", :type), load("enum").first)
    end

    def load_enum()
      Enum.new(load("choice"))
    end

    def load_choice()
      Choice.new(attr("name", :name), attr("value", :int))
    end

    def load_field()
      Field.new(attr("name", :name), attr("type", :type))
    end

    def load_struct()
      Struct.new(attr("name", :decl), attr("code", :code), attr("size", :int),
                 attr("pack", :int), load("field"))
    end

    def load_action(cls)
      cls.new(attr("name", :decl), attr("code", :code), 0, 2, load("field"))
    end

    def load_control()
      load_action(Control)
    end

    def load_command()
      result = attr("type", :type, nil, "result")
      result = attr("name", :type, nil, "result/struct") if result.nil?
      segs = load("segments")
      cmd = load_action(Command)
      cmd.result = result
      cmd.payload = !segs.empty?
      return cmd
    end

    def load_result()
      true
    end

    def load_segments()
      true
    end

  end

  def self.spec_cache(specfile)
      File::join(File::dirname(__FILE__), "spec_cache",
                 File::basename(specfile, ".xml") + ".rb_marshal")
  end

  # XXX: could be shared
  def self.load(spec = nil)
    return spec if spec.is_a?(Qpid::Spec010::Spec)
    if spec.nil?
      # FIXME: Need to add a packaging setup in here so we know where
      # the installed spec is going to be.
      specfile = nil
      if ENV['AMQP_SPEC']
        specfile = ENV['AMQP_SPEC']
      else
        require "qpid/config"
        specfile = Qpid::Config.amqp_spec
      end
    else
      specfile = spec
    end

    specfile_cache = spec_cache(specfile)
    # FIXME: Check that cache is newer than specfile
    if File::exist?(specfile_cache)
      begin
        spec = File::open(specfile_cache, "r") do |f|
          Marshal::load(f)
        end
        return spec
      rescue
        # Ignore, will load from XML
      end
    end

    doc = File::open(specfile, "r") { |f| Document.new(f) }
    spec = Loader010.new().load(doc.root)
    spec.traverse! do |o|
      if o.is_a?(Reference)
        o.resolve(spec)
      else
        o
      end
    end

    spec.children.each { |c| c.parent = spec }

    begin
      FileUtils::mkdir_p(File::dirname(specfile_cache))
      File::open(specfile_cache, "w") { |f| Marshal::dump(spec, f) }
    rescue
      # Ignore, we are fine without the cached spec
    end
    return spec
  end

end
