require 'digest'
require 'norikra/field'

module Norikra
  class FieldSet
    attr_accessor :summary, :fields
    attr_accessor :target, :level

    # fieldset doesn't have container fields
    def initialize(fields, default_optional=nil, rebounds=0)
      @fields = {}
      # fields.keys are raw key for container access chains
      fields.keys.each do |key|
        data = fields[key]
        if data.is_a?(Norikra::Field)
          @fields[data.name] = data
        elsif data.is_a?(Hash)
          type = data[:type].to_s
          optional = data.has_key?(:optional) ? data[:optional] : default_optional
          @fields[key.to_s] = Field.new(key.to_s, type, optional)
        elsif data.is_a?(String) || data.is_a?(Symbol)
          @fields[key.to_s] = Field.new(key.to_s, data.to_s, default_optional)
        else
          raise ArgumentError, "FieldSet.new argument class unknown: #{fields.class}"
        end
      end
      self.update_summary

      @target = nil
      @level = nil
      @rebounds = rebounds
      @event_type_name = nil
    end

    def dup
      fields = Hash[@fields.map{|key,field| [key, {:type => field.type, :optional => field.optional}]}]
      self.class.new(fields, nil, @rebounds)
    end

    def self.leaves(container)
      # returns list of [ [key-chain-items-flatten-list, value] ]
      dig = Proc.new do |obj|
        if obj.is_a?(Array)
          ary = []
          obj.each_with_index do |v,i|
            if v.is_a?(Hash) || v.is_a?(Array)
              ary += dig.call(v).map{|chain| [i] + chain}
            else
              ary.push([i, v])
            end
          end
          ary
        else # Hash
          obj.map {|k,v|
            if v.is_a?(Hash) || v.is_a?(Array)
              dig.call(v).map{|chain| [k] + chain}
            else
              [[k, v]]
            end
          }.reduce(:+)
        end
      end
      dig.call(container)
    end

    def self.field_names_key(data, fieldset=nil, strict=false, additional_fields=[])
      if !fieldset && strict
        raise RuntimeError, "strict(true) cannot be specified with fieldset=nil"
      end

      unless fieldset
        return data.keys.sort.join(',')
      end

      keys = []
      optionals = []

      fieldset.fields.each do |key,field|
        if field.optional?
          optionals.push(field.name)
        else
          keys.push(field.name)
        end
      end
      optionals += additional_fields

      Norikra::FieldSet.leaves(data).each do |chain|
        value = chain.pop
        key = Norikra::Field.regulate_key_chain(chain).join('.')
        unless keys.include?(key)
          if optionals.include?(key) || (!strict && chain.size == 1)
            keys.push(key)
          end
        end
      end

      keys.sort.join(',')
    end

    def field_names_key
      self.class.field_names_key(@fields)
    end

    def update_summary
      @summary = @fields.keys.sort.map{|k| @fields[k].escaped_name + ':' + @fields[k].type}.join(',')
      self
    end

    def update(fields, optional_flag)
      fields.each do |field|
        @fields[field.name] = field.dup(optional_flag)
      end
      self.update_summary
    end

    #TODO: have a bug?
    def ==(other)
      return false if self.class != other.class
      self.summary == other.summary
    end

    def definition
      d = {}
      @fields.each do |key, field|
        d[field.escaped_name] = field.type
      end
      d
    end

    def subset?(other) # self is subset of other (or not)
      (self.fields.keys - other.fields.keys).size == 0
    end

    def event_type_name
      @event_type_name.dup
    end

    def bind(target, level, update_type_name=false)
      @target = target
      @level = level
      prefix = case level
               when :base then 'b_'
               when :query then 'q_'
               when :data then 'e_' # event
               else
                 raise ArgumentError, "unknown fieldset bind level: #{level}, for target #{target}"
               end
      @rebounds += 1 if update_type_name

      @event_type_name = prefix + Digest::MD5.hexdigest([target, level.to_s, @rebounds.to_s, @summary].join("\t"))
      self
    end

    def rebind(update_type_name)
      self.dup.bind(@target, @level, update_type_name)
    end

    def format(data)
      # all keys of data should be already known at #format (before #format, do #refer)
      ret = {}
      @fields.each do |key,field|
        ret[field.escaped_name] = field.format(field.value(data))
      end
      ret
    end
  end
end