module Fin
  # Represents business domain model for a single item (Quote, Deal, Instrument, etc...)
  # Currently it is only used to extract common functionality from record wrappers,
  # down the road the goal is to add ActiveModel compatibility
  class Model

    include Enumerable

    def self.attribute_types
      @attribute_types ||= (superclass.attribute_types.dup rescue {})
    end

    def self.model_class_id value = nil
      if value
        @model_class_id ||= value
        model_classes[@model_class_id] = self
      else
        @model_class_id
      end
    end

    def self.model_classes
      @model_classes ||= (superclass.model_classes rescue {}) #shared list for all subclasses
    end

    def self.property prop_hash
      prop_hash.each do |arg, type|
        aliases = [arg].flatten
        name = aliases.shift
        instance_eval do

          attribute_types[name.to_s] = type.to_s

          define_method(name) do
            @attributes[name]
          end

          define_method("#{name}=") do |value|
            @attributes[name] = value
          end

          aliases.each do |ali|
            alias_method "#{ali}", name
            alias_method "#{ali}=", "#{name}="
          end
        end
      end

      # Using static calls, create class method extracting attributes from raw records
      attribute_extractor = attribute_types.map do |name, type|
        case type
           # TODO: Using indexes (...ByIndex) instead of names gives ~60% speed boost
          when /^i[14]/
            "rec.GetValAsLong('#{name}')"
          when /^i8/
            "rec.GetValAsVariant('#{name}')"
          when /^[df]/
            "rec.GetValAsString('#{name}').to_f"
          when /^[c]/
            "rec.GetValAsString('#{name}')"
          when /^[t]/ # "2009/12/01 12:35:44.785" => 20091201123544785
            "rec.GetValAsVariant('#{name}')"
          else
            raise "Unrecognized attribute type: #{name} => #{type}"
        end
      end.join(",\n")

      extractor_body = "def self.extract_attributes rec
                          [#{attribute_extractor}]
                        end"

#      puts "In #{self}:, #{extractor_body"
      instance_eval extractor_body
    end

    def self.from_record rec
      new *extract_attributes(rec)
    end

    # Unpacks attributes into appropriate Model subclass
    def self.from_msg msg
      class_id = msg.first
      model_classes[class_id].new *msg[1..-1]
    end

    # Extracts attributes from record into a serializable format (Array)
    # Returns an Array where 1st element is a model_class_id of our Model subclass,
    # followed by a list of arguments to its initialize. Class method!
    def self.to_msg rec
      extract_attributes(rec).unshift(model_class_id)
    end

    # Converts OBJECT attributes into a serializable format (Array)
    # Returns an Array where 1st element is a model_class_id of our Model subclass,
    # followed by a list of arguments to its initialize. Instance method!
    def to_msg
      inject([self.class.model_class_id]) { |array, (name, _)| array << send(name) }
    end

    # TODO: Builder pattern, to avoid args Array creation on each initialize?
    def initialize *args
      @attributes = {}
      opts = args.last.is_a?(Hash) ? args.pop : {}
      each_with_index { |(name, _), i| send "#{name}=", args[i] } unless args.empty?
      opts.each { |name, value| send "#{name}=", value }
    end

    def each
      if block_given?
        self.class.attribute_types.each { |name, _| yield [name, send(name)] }
      else
        self.class.attribute_types.map { |name, _| [name, send(name)].to_enum }
      end
    end

    alias each_property each

    def inspect divider=','
      map { |property, value| "#{property}=#{value}" }.join(divider)
    end

    # TODO: DRY principle: there should be one authoritative source for everything...
    # TODO: Should such source be schema file, or Model code?
    # TODO: Maybe, Model should just read from schema file at init time?

    # All P2 records carry these properties
    property [:replID, :repl_id] => :i8,
             [:replRev, :repl_rev, :rev] => :i8,
             [:replAct, :repl_act] => :i8

    def index
      object_id # TODO: @repl_id?
    end

    # Fin::Model itself has model_class_id 0
    model_class_id 0
  end
end