require 'objspace'

module Cenit
  class Utility

    class Proxy

      def initialize(obj, attributes = {})
        @obj = obj
        @attributes = attributes
      end

      def method(sym)
        @obj.method(sym)
      end

      def method_missing(symbol, *args, &block)
        if @attributes.has_key?(symbol)
          @attributes[symbol]
        elsif @obj.respond_to?(symbol)
          @obj.send(symbol, *args, &block)
        else
          super
        end
      end

      def respond_to?(*args)
        @attributes.has_key?(args[0]) || @obj.respond_to?(*args) || super
      end
    end

    class << self
      def save(record, options = {})
        saved = options[:saved_collector] || Set.new
        if bind_references(record, options.delete(:bind_references))
          success =
            if record.try(:save_self_before_refs)
              record.save(options) && save_references(record, options, saved)
            else
              save_references(record, options, saved) && record.save(options)
            end
          if success
            true
          else
            for_each_node_starting_at(record, stack: stack = []) do |obj|
              obj.errors.each do |err|
                attr_ref = "#{obj.orm_model.data_type.title}" +
                           ((name = obj.try(:name)) || (name = obj.try(:title)) ? " #{name} on attribute " : " property ") +
                           err.attribute.to_s #TODO Trunc and do html safe for long values, i.e, XML Schemas ---> + ((v = obj.try(attribute)) ? "'#{v}'" : '')
                path = ''
                stack.reverse_each do |node|
                  if !node[:record].is_a?(Mongoff::Record) && node[:referenced]
                    node[:record].errors.add(node[:attribute], "with error on #{path}#{attr_ref} (#{err.message})")
                  end
                  path = node[:record].orm_model.data_type.title + ' -> '
                end
              end
            end
            saved.delete_if { |obj| !obj.instance_variable_get(:@dynamically_created) }
            for_each_node_starting_at(record) do |obj|
              saved << obj if obj.instance_variable_get(:@dynamically_created)
            end
            saved.each do |obj|
              if (obj = (obj.reload rescue nil))
                obj.delete
              end
            end unless options.has_key?(:saved_collector)
            false
          end
        else
          false
        end
      end

      def bind_references(record, options = {})
        options ||= {}
        references = {}
        for_each_node_starting_at(record, options) do |obj|
          ::Setup::Optimizer.instance.regist_data_types(obj)
          if (record_refs = obj.instance_variable_get(:@_references)).present?
            references[obj] = record_refs
          end
        end

        visited = options[:visited]
        bound_records = []

        lazy_models = [Setup::MappingConverter]

        while_modifying references do
          while_modifying references do
            references.each do |obj_waiting, to_bind|
              next if lazy_models.include?(obj_waiting.class)
              visited.each do |obj|
                to_bind.each do |property_name, property_binds|
                  is_array = property_binds.is_a?(Array) ? true : (property_binds = [property_binds]; false)
                  property_binds.each do |property_bind|
                    if obj.is_a?(property_bind[:model]) && match?(obj, property_bind[:criteria])
                      if is_array
                        unless (array_property = obj_waiting.send(property_name))
                          obj_waiting.send("#{property_name}=", array_property = [])
                        end
                        array_property << obj
                      else
                        obj_waiting.send("#{property_name}=", obj)
                      end
                      property_binds.delete(property_bind)
                    end
                    to_bind.delete(property_name) if property_binds.empty?
                  end
                  if to_bind.empty?
                    bound_records << obj_waiting
                    references.delete(obj_waiting)
                  end
                end
              end
            end
          end

          references.each do |obj, to_bind|
            next if lazy_models.include?(obj.class)
            to_bind.each do |property_name, property_binds|
              is_array = property_binds.is_a?(Array) ? true : (property_binds = [property_binds]; false)
              property_binds.each do |property_bind|
                if (value = Cenit::Utility.find_record(property_bind[:criteria], obj.orm_model.property_model(property_name)))
                  if is_array
                    unless (association = obj.send(property_name)).include?(value)
                      association << value
                    end
                  else
                    obj.send("#{property_name}=", value)
                  end
                  property_binds.delete(property_bind)
                elsif !options[:skip_error_report]
                  message = "#{property_bind[:model]} reference not found with criteria #{property_bind[:criteria].to_json}"
                  obj.errors.add(property_name, message)
                  # TODO Report errors to parents
                  # message = "#{obj.class} on attribute #{property_name} #{message}"
                  # stack.reverse_each do |node|
                  #   message = "#{node[:record].class} '#{node[:record].name}' on attribute #{node[:attribute]} -> #{message}"
                  #   node[:record].errors.add(node[:attribute], message)
                  # end
                end
              end
              to_bind.delete(property_name) if property_binds.empty?
            end
            if to_bind.empty?
              bound_records << obj
              references.delete(obj)
            end
          end

          do_it = nil


          if bound_records.empty?
            if lazy_models.present?
              lazy_models.shift
              do_it = :again
            end
          else
            bound_records.each do |record|
              visited.delete(record)
              for_each_node_starting_at(record, options) do |obj|
                if (record_refs = obj.instance_variable_get(:@_references)).present?
                  references[obj] = record_refs
                end
              end
            end
            bound_records.clear
            do_it = :again
          end

          do_it
        end
        options.delete(:visited)
        record.errors.blank?
      end

      def while_modifying(hash)
        do_it = nil
        start_size = hash.size + 1
        while do_it == :again || (hash.present? && hash.size < start_size)
          start_size = hash.size
          do_it = yield
        end
      end

      def match?(obj, criteria)
        criteria.each do |property_name, value|
          property_value =
            case obj
              when Hash
                obj[property_name]
              else
                obj.try(property_name)
            end
          if value.is_a?(Hash)
            return false unless match?(property_value, value)
          else
            property_value =
              case property_value
                when BSON::ObjectId
                  value = value.to_s
                  property_value.to_s
                else
                  property_value
              end
            return false unless property_value == value
          end
        end
        true
      end

      def for_each_node_starting_at(record, options = {}, &block)
        stack = options[:stack]
        unless (visited = options[:visited])
          visited = options[:visited] = Set.new
        end
        visited << record
        block.yield(record) if block
        if (orm_model = record.try(:orm_model))
          stored_properties = orm_model.stored_properties_on(record)
          orm_model.for_each_association do |relation|
            next unless stored_properties.include?(relation[:name].to_s)
            if (values = record.send(relation[:name]))
              stack << { record: record, attribute: relation[:name], referenced: !relation[:embedded] } if stack
              values = [values] unless values.is_a?(Enumerable)
              values.each do |value|
                next if visited.include?(value)
                if (if_opt = options[:if])
                  next unless if_opt.call(value)
                end
                for_each_node_starting_at(value, options, &block)
              end
              stack.pop if stack
            end
          end
        end
      end

      def save_references(record, options, saved, visited = Set.new)
        # TODO: Propagate error to parent relation...
        return true if visited.include?(record)
        visited << record
        if record.is_a?(Setup::Collection)
          Setup::Collection::COLLECTING_PROPERTIES.each do |property|
            record.send(property).each do |value|
              next unless visited.exclude?(value) && value.changed?
              new_record = value.new_record?
              if value.save(options)
                if new_record || value.instance_variable_get(:@dynamically_created)
                  value.instance_variable_set(:@dynamically_created, true)
                  options[:create_collector] << value if options[:create_collector]
                else
                  options[:update_collector] << value if options[:update_collector]
                end
                saved << value
              else
                return false
              end
            end
          end
        elsif (model = record.try(:orm_model))
          model.for_each_association do |relation|
            next if model.excluded_relation?(relation[:name])
            if (values = record.send(relation[:name]))
              values = [values] unless values.is_a?(Enumerable)
              values_to_save = []
              values.each do |value|
                if value.changed? && !visited.include?(value)
                  return false unless save_references(value, options, saved, visited)
                  values_to_save << value
                end
              end
              values_to_save.each do |value|
                unless saved.include?(value)
                  new_record = value.new_record?
                  if value.changed?
                    if value.save(options)
                      if new_record || value.instance_variable_get(:@dynamically_created)
                        value.instance_variable_set(:@dynamically_created, true)
                        options[:create_collector] << value if options[:create_collector]
                      else
                        options[:update_collector] << value if options[:update_collector]
                      end
                      saved << value
                    else
                      return false
                    end
                    saved << value
                  end
                end
              end unless relation[:embedded]
            end
          end
        end
        true
      end

      def find_record(conditions, *scopes)
        scopes.each do |original_scope|
          scope = original_scope
          match_conditions = {}
          begin
            scope_klass =
              begin
                scope.klass
              rescue
                scope.model
              end
            scope_associations = scope_klass.get_associations
          rescue
            scope_klass = nil
            scope_associations = nil
          end
          conditions.each do |key, value|
            if value.is_a?(Hash)
              if scope_associations && (association = scope_associations[key])
                scope = scope.where(association.foreign_key => { '$in' => associated_ids(association, value) })
              else
                match_conditions[key] = value
              end
            elsif scope.respond_to?(:where)
              scope = scope.where(key => value)
            else
              scope = scope.select do |record|
                if (record_model = record.try(:orm_model))
                  record[key] == record_model.mongo_value(value, key)
                else
                  record[key] == value
                end
              end
            end
          end
          record =
            if scope.respond_to?(:detect)
              scope
            elsif scope.respond_to?(:all)
              scope.all
            else
              []
            end.detect { |record| match?(record, match_conditions) }
          if record
            if original_scope.is_a?(Enumerable) && (o_r = original_scope.detect { |item| item == record })
              return o_r
            end
            return record
          end
        end
        nil
      end

      def associated_ids(association, criteria)
        associations =
          begin
            association.klass.get_associations
          rescue
            nil
          end
        new_criteria = {}
        criteria.each do |key, value|
          if (a = associations[key])
            criteria.delete(key)
            a_criteria =
              if a == association
                value
              else
                { key => value }
              end
            new_criteria[a.foreign_key] = { '$in' => associated_ids(a, a_criteria) }
          end
        end if associations
        criteria.merge!(new_criteria)
        association.klass.where(criteria).collect(&:id)
      end

      def deep_remove(hash, key)
        if hash.is_a?(Hash)
          hash.inject({}) do |h, (k, v)|
            h[k] = deep_remove(v, key) unless k == key
            h
          end
        else
          hash
        end
      end

      def stringfy(obj)
        if obj.is_a?(Hash)
          hash = {}
          obj.each { |key, value| hash[key.to_s] = stringfy(value) }
          hash
        elsif obj.is_a?(Array)
          obj.collect { |value| stringfy(value) }
        else
          obj.is_a?(Symbol) ? obj.to_s : obj
        end
      end

      def json_object?(obj, options = {})
        case obj
          when Hash
            if options[:recursive]
              obj.keys.each { |k| return false unless k.is_a?(String) }
              obj.values.each { |v| return false unless json_object?(v) }
            end
            true
          when Array
            obj.each { |v| return false unless json_object?(v) } if options[:recursive]
            true
          else
            [Integer, BigDecimal, Float, String, TrueClass, FalseClass, Mongoid::Boolean, NilClass, BSON::ObjectId, BSON::Binary].any? { |klass| obj.is_a?(klass) }
        end
      end

      def array_hash_merge(val1, val2, options = {}, &block)
        if val1.is_a?(Array) && val2.is_a?(Array)
          if options[:array_uniq]
            (val2 + val1).uniq(&block)
          else
            val1 + val2
          end
        elsif val1.is_a?(Hash) && val2.is_a?(Hash)
          val1.deep_merge(val2) { |_, val1, val2| array_hash_merge(val1, val2) }
        else
          val2
        end
      end

      # TODO Remove this methods (used by rails_admin custom fields) since blank string are actually valid JSON values
      def json_value_of(value)
        return value unless value.is_a?(String)
        value = value.strip
        if value.blank?
          nil
        elsif value.start_with?('"') && value.end_with?('"')
          value[1..value.length - 2]
        else
          begin
            JSON.parse(value)
          rescue
            if (v = value.to_i).to_s == value
              v
            elsif (v = value.to_f).to_s == value
              v
            else
              case value
                when 'true'
                  true
                when 'false'
                  false
                else
                  value
              end
            end
          end
        end
      end

      def eql_content?(a, b, key = nil, &block)
        case a
          when Hash
            if b.is_a?(Hash)
              if a.size < b.size
                a, b = b, a
              end
              a.each do |k, value|
                return false unless b.key?(k) && eql_content?(value, b[k], k, &block)
              end
            else
              return block && block.call(*(block.arity == 3 ? [a, b, key] : [a, b]))
            end
          when Array
            if b.is_a?(Array) && a.length == b.length
              a = a.dup
              b = b.dup
              until a.empty?
                a_value = a.shift
                b_len = b.length
                b.delete_if { |b_value| eql_content?(a_value, b_value, &block) }
                if b.length < b_len
                  a.delete_if { |value| eql_content?(a_value, value, &block) }
                end
                return false unless a.length == b.length
              end
            else
              return block && block.call(*(block.arity == 3 ? [a, b, key] : [a, b]))
            end
          else
            return a.eql?(b) || (block && block.call(*(block.arity == 3 ? [a, b, key] : [a, b])))
        end
        true
      end
    end
  end
end
