require 'query_builder'
# Since QueryNode uses a special scope (secure_scope) that needs to be evaluated _in the query string_, you cannot
# use query.sql. You have to use: eval query.to_s. This is not an issue since normal useage for QueryNode is to be
# compilated into an ERB template.

module Zena
  module Use
    class QueryNode < QueryBuilder::Processor

      def initialize(query, opts = {})
        @uses_node_name = false
        @table_name = 'nodes'
        @node_name  = opts[:node_name]
        # list of dyna_attributes keys allready in the filter
        @dyn_keys   = {}
        super(query, opts)
        # Raw filters are statements prepared that should not be further processed except for table_name replacement.
        parse_raw_filters(opts[:raw_filters])
      end

      # Build joins and filters from a relation.
      def parse_relation(rel, context)
        # join_relation first so we can overwrite 'class' finders (images) with a relation.
        unless join_relation(rel, context) || context_relation(rel, context)
          @errors << "unknown relation '#{rel}'"
        end
      end

      def after_parse
        @where.unshift "(\#{#{@node_name}.secure_scope('#{table}')})"
        if @tables.include?('links')
          link_table = table('links')
          @select << "#{link_table}.id AS link_id,#{Zena::Use::Relations::LINK_ATTRIBUTES.map {|l| "#{link_table}.#{l} AS l_#{l}"}.join(',')}"
        elsif @errors_unless_safe_links
          @errors += @errors_unless_safe_links
        end
        @distinct = true if @tables.include?('versions')
      end

      private
        # Make sure all alternate queries include "links.id = 0" (dummy link)
        def fix_where_list(where_list)
          return unless @tables.include?('links')
          where_list.each do |f|
            unless f =~ /links\./
              f << " AND links.id = 0"
            end
          end
          true
        end

        # Used to resolve 'in' clauses ('in project', 'in parent', etc)
        def context_filter_fields(rel, is_last = false)
          case rel
          when 'self'
            ['parent_id', 'id']
          when 'parent'
            is_last ? ['parent_id', 'parent_id'] : ['parent_id', 'id']
          when 'project'
            is_last ? ['project_id', 'project_id'] : ['project_id', 'id']
          when 'section'
            is_last ? ['section_id', 'section_id'] : ['section_id', 'id']
          when 'site', main_table
            :void
          else
            nil
          end
        end

        def parse_change_class(rel, is_last)
          case rel
          when 'comment', 'comments'
            # parse_context(default_context_filter, true) if is_last
            # after_parse
            add_table('discussions')
            add_table('comments')
            @where << "#{table('discussions')}.node_id = #{table('nodes')}.id"
            @where << "#{table('comments')}.discussion_id = #{table('discussions')}.id"
            after_parse
            return Zena::Use::QueryComment
          else
            return nil
          end
        end

        # Filters that need a join
        def join_relation(rel, context, new_table_alias = nil, previous_table_alias = nil)
          new_table_alias      ||= table(main_table)
          previous_table_alias ||= table(main_table, -1)

          if rel == main_table || rel == 'children'
            # dummy clauses
            parse_context(default_context_filter) unless context
            return :void
          end

          if rel = RelationProxy.find_by_role(rel.singularize)
            # We cannot use a LEFT JOIN here because it will totally mess up if we merge alternate queries
            add_table('links')
            # (= other_side = result) target <-- source (= link_side = caller)
            if context && context != 'self'
              # tagged in project (not equal to 'tagged from nodes in project')
              # remove caller join
              @distinct = true
              @where << "#{field_or_attr('id', new_table_alias)} = #{table('links')}.#{rel.other_side} AND #{table('links')}.relation_id = #{rel[:id]}"
            else
              @where << "#{field_or_attr('id', new_table_alias)} = #{table('links')}.#{rel.other_side} AND #{table('links')}.relation_id = #{rel[:id]} AND #{table('links')}.#{rel.link_side} = #{field_or_attr('id', previous_table_alias)}"
            end
          else
            nil
          end
        end

        def map_literal(value, env = :sql)
          if value =~ /(.*?)\[(visitor|param|node):(\w+)\](.*)/
            val_start = $1 == '' ? '' : "#{$1.inspect} +"
            val_end   = $4 == '' ? '' : "+ #{$4.inspect}"
            case $2
            when 'visitor'
              value = env == :sql ? insert_bind("#{val_start}Node.zafu_attribute(visitor.contact, #{$3.inspect})#{val_end}") : nil
            when 'param'
              value = env == :sql ? insert_bind("#{val_start}params[:#{$3}].to_s#{val_end}") : "params[:#{$3}]"
            when 'node'
              @uses_node_name = true
              value = env == :sql ? insert_bind("#{val_start}#{@node_name}.safe_read(#{$3.inspect}).to_s#{val_end}") : "#{@node_name}.safe_read(#{$3.inspect})"
            end
          else
            value = env == :sql ? quote(value) : nil
          end
        end

        # Translate fields used for query/sort/grouping (context parameter) into something useable by SQL. Add the appropriate tables when needed.
        def map_field(field, table_name = table, context = nil)
          return map_literal("[#{field}]") if field =~ /\Aparam:/
          case field[0..1]
          when 'd_'
            # DYNAMIC ATTRIBUTE
            key = field[2..-1]
            key, function = parse_sql_function_in_field(key)
            key = Zena::Db.sql_function(function, dyn_value('versions', key, context))
          when 'c_'
            # CONTENT TABLE
            field = field[2..-1]
            # FIXME: implement #41
            nil
          when 'v_'
            # VERSION
            key = field[2..-1]
            key, function = parse_sql_function_in_field(key)
            if Version.safe_method_type([key]) && Version.column_names.include?(key)
              vtable_name = needs_table('nodes', 'versions', "TABLE1.id = TABLE2.node_id")
              Zena::Db.sql_function(function, "#{vtable_name}.#{key}")
            else
              # bad version attribute
              nil
            end
          when 'l_'
            key, function = parse_sql_function_in_field(field)
            if key == 'l_status' || key == 'l_comment' || key == 'l_date' || (key == 'l_id' && [:order, :group].include?(context))
              @errors_unless_safe_links ||= []
              @errors_unless_safe_links << "cannot use link field '#{key}' in this query" unless (key == 'l_id' && context == :order)
              # ok
              Zena::Db.sql_function(function, "#{table('links')}.#{key[2..-1]}")
            else
              # bad attribute
              nil
            end
          else
            # NODE
            key, function = parse_sql_function_in_field(field)
            if context == :filter
              if map_def = @@filter_fields[key]
                if table_def = map_def[:table]
                  table_to_use = needs_table(*table_def)
                else
                  table_to_use = table_name
                end
                Zena::Db.sql_function(function, "#{table_to_use}.#{map_def[:key]}")
              elsif (Node.safe_method_type([key]) && Node.column_names.include?(key))
                Zena::Db.sql_function(function, "#{table_name}.#{key}")
              elsif key =~ /^(.*)_ids?$/
                # tag_id = 33  ===> join links as lk, nodes as tt .......
                rel = $1

                if RelationProxy.find_by_role(rel.singularize)
                  add_table('jnode', 'nodes')
                  join_relation(rel, nil, table('jnode'), table('nodes'))
                  "#{table('jnode')}.zip"
                else
                  nil
                end
              else
                nil
              end
            else
              if ['id', 'parent_id','project_id','section_id'].include?(key) || (Node.safe_method_type([key]) && Node.column_names.include?(key))
                Zena::Db.sql_function(function, "#{table_name}.#{key}")
              else
                # bad attribute
                nil
              end
            end
          end
        end

        def valid_field?(table_name, fld)
          # FIXME: security !
          true
        end

        def parse_paginate_clause(paginate)
          return @offset unless paginate
          if !@limit
            # TODO: raise error ?
            @errors << "invalid paginate clause '#{paginate}' (used without limit)"
            nil
          elsif (fld = map_literal("[param:#{paginate}]", :ruby)) && (page_size = @limit[/ LIMIT (\d+)/,1])
            @page_size = [2,page_size.to_i].max
            " OFFSET #{insert_bind("((#{fld}.to_i > 0 ? #{fld}.to_i : 1)-1)*#{page_size.to_i}")}"
          else
            @errors << "invalid paginate clause '#{paginate}'"
            nil
          end
        end

        # When a field is defined as log_at:year, return [log_at, year].
        def parse_sql_function_in_field(field)
          if field =~ /\A(\w+):(\w+)\Z/
            if ['year'].include?($2)
              [$1,$2]
            else
              [$1]
            end
          else
            [field]
          end
        end

        def parse_raw_filters(filters)
          return unless filters
          filters.each do |f|
            all_ok = true
            filter = f.gsub(/TABLE_NAME(\[(\w+)\]|)/) do
              if $2
                if @table_counter[$2]
                  table($2)
                else
                  all_ok = false
                  @errors << "invalid table_name '#{$2}' in raw filter"
                  @errors.uniq!
                end
              else
                table
              end
            end
            @where << filter if all_ok
          end
        end

        def dyn_value(table_name, key, context)
          @dyn_keys[table_name] ||= {}
          @dyn_keys[table_name][key] ||= begin
            needs_table('nodes', 'versions', "TABLE1.id = TABLE2.node_id")
            dtable = needs_join_table('versions', 'LEFT', 'dyn_attributes', "TABLE1.id = TABLE2.owner_id AND TABLE2.key = #{quote(key)}", "versions=dyn_attributes=#{key}")
            "#{dtable}.value"
          end
        end

        def extract_custom_query(list)
          super.singularize
        end

        def quote(literal)
          Node.connection.quote(literal)
        end
    end # QueryNode
  end # Use
end # Zena
