module Praxis::Mapper

  class SelectorGeneratorNode
    attr_reader :select, :model, :resource, :tracks

    def initialize(resource)
      @resource = resource

      @select = Set.new
      @select_star = false
      @tracks = Hash.new 
    end

    def add(fields)
      fields.each do |name, field|
        map_property(name, field)
      end
      self
    end

    def map_property(name, fields)
      praxis_compat_model = resource.model && resource.model.respond_to?(:_praxis_associations)
      if resource.properties.key?(name)
        add_property(name, fields)
      elsif praxis_compat_model && resource.model._praxis_associations.key?(name)
        add_association(name, fields)
      else
        add_select(name)
      end
    end

    def add_association(name, fields)
      
      association = resource.model._praxis_associations.fetch(name) do
        raise "missing association for #{resource} with name #{name}"
      end
      associated_resource = resource.model_map[association[:model]]
      unless associated_resource
        raise "Whoops! could not find a resource associated with model #{association[:model]} (root resource #{resource})"
      end
      # Add the required columns in this model to make sure the association can be loaded
      association[:local_key_columns].each {|col| add_select(col) }
      
      node = SelectorGeneratorNode.new(associated_resource)
      unless association[:remote_key_columns].empty?
        # Make sure we add the required columns for this association to the remote model query
        fields = {} if fields == true
        new_fields_as_hash = association[:remote_key_columns].each_with_object({}) do|name, hash|
          hash[name] = true
        end
        fields = fields.merge(new_fields_as_hash) 
      end

      node.add(fields) unless fields == true

      self.merge_track(name, node)
    end

    def add_select(name)
      return @select_star = true if name == :*
      return if @select_star
      @select.add name
    end

    def add_property(name, fields)
      dependencies = resource.properties[name][:dependencies]
      # Always add the underlying association if we're overriding the name...
      praxis_compat_model = resource.model && resource.model.respond_to?(:_praxis_associations)
      if praxis_compat_model && resource.model._praxis_associations.key?(name)
        add_association(name, fields)
      end
      if dependencies
        dependencies.each do |dependency|
          # To detect recursion, let's allow mapping depending fields to the same name of the property
          # but properly detecting if it's a real association...in which case we've already added it above
          if dependency == name
            unless praxis_compat_model && resource.model._praxis_associations.key?(name)
              add_select(name)
            end
          else
            apply_dependency(dependency)
          end
        end
      end

      head, *tail = resource.properties[name][:through]
      return if head.nil?

      new_fields = tail.reverse.inject(fields) do |thing, step|
        {step => thing}
      end

      add_association(head, new_fields)
    end

    def apply_dependency(dependency)
      case dependency
      when Symbol
        map_property(dependency, true)
      when String
        head, *tail = dependency.split('.').collect(&:to_sym)
        raise "String dependencies can not be singular" if tail.nil?

        add_association(head, tail.reverse.inject({}) { |hash, dep| { dep => hash } })
      end
    end

    def merge_track( track_name, node )
      raise "Cannot merge another node for association #{track_name}: incompatible model" unless node.model == self.model

      existing = self.tracks[track_name]
      if existing
        node.select.each do|col_name|
          existing.add_select(col_name)
        end
        node.tracks.each do |name, n|
          existing.merge_track(name, n)
        end
      else
        self.tracks[track_name] = node
      end
    end

    def dump
      hash = {}
      hash[:model] = resource.model
      if !@select.empty? || @select_star
        hash[:columns] = @select_star ? [ :* ] : @select.to_a 
      end
      unless @tracks.empty?
        hash[:tracks] = @tracks.each_with_object({}) {|(name, node), hash|  hash[name] = node.dump }
      end
      hash
    end
  end

  # Generates a set of selectors given a resource and
  # list of resource attributes.
  class SelectorGenerator
    # Entry point
    def add(resource, fields)
      @root = SelectorGeneratorNode.new(resource)
      @root.add(fields)
      self
    end

    def selectors
      @root
    end
  end
end
