# frozen_string_literal: true

# Redmine - project management software
# Copyright (C) 2006-  Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

class TimeEntryQuery < Query
  self.queried_class = TimeEntry
  self.view_permission = :view_time_entries

  self.available_columns = [
    QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
    QueryColumn.new(:spent_on, :sortable => ["#{TimeEntry.table_name}.spent_on", "#{TimeEntry.table_name}.created_on"], :default_order => 'desc', :groupable => true),
    TimestampQueryColumn.new(:created_on, :sortable => "#{TimeEntry.table_name}.created_on", :default_order => 'desc', :groupable => true),
    QueryColumn.new(:tweek, :sortable => ["#{TimeEntry.table_name}.tyear", "#{TimeEntry.table_name}.tweek"], :caption => :label_week),
    QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement}),
    QueryColumn.new(:user, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
    QueryColumn.new(:activity, :sortable => "#{TimeEntryActivity.table_name}.position", :groupable => true),
    QueryColumn.new(:issue, :sortable => "#{Issue.table_name}.id", :groupable => true),
    QueryAssociationColumn.new(:issue, :tracker, :caption => :field_tracker, :sortable => "#{Tracker.table_name}.position"),
    QueryAssociationColumn.new(:issue, :parent, :caption => :field_parent_issue, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc'),
    QueryAssociationColumn.new(:issue, :status, :caption => :field_status, :sortable => "#{IssueStatus.table_name}.position"),
    QueryAssociationColumn.new(:issue, :category, :caption => :field_category, :sortable => "#{IssueCategory.table_name}.name"),
    QueryAssociationColumn.new(:issue, :fixed_version, :caption => :field_fixed_version, :sortable => Version.fields_for_order_statement),
    QueryColumn.new(:comments),
    QueryColumn.new(:hours, :sortable => "#{TimeEntry.table_name}.hours", :totalable => true),
  ]

  def initialize(attributes=nil, *args)
    super(attributes)
    self.filters ||= {'spent_on' => {:operator => "*", :values => []}}
  end

  def initialize_available_filters
    add_available_filter "spent_on", :type => :date_past
    add_available_filter(
      "project_id",
      :type => :list, :values => lambda {project_values}
    ) if project.nil?
    if project && !project.leaf?
      add_available_filter(
        "subproject_id",
        :type => :list_subprojects,
        :values => lambda {subproject_values})
    end
    add_available_filter("issue_id", :type => :tree, :label => :label_issue)
    add_available_filter(
      "issue.tracker_id",
      :type => :list,
      :name => l("label_attribute_of_issue", :name => l(:field_tracker)),
      :values => lambda {trackers.map {|t| [t.name, t.id.to_s]}})
    add_available_filter(
      "issue.parent_id",
      :type => :tree,
      :name => l("label_attribute_of_issue", :name => l(:field_parent_issue)))
    add_available_filter(
      "issue.status_id",
      :type => :list,
      :name => l("label_attribute_of_issue", :name => l(:field_status)),
      :values => lambda {issue_statuses_values})
    add_available_filter(
      "issue.fixed_version_id",
      :type => :list,
      :name => l("label_attribute_of_issue", :name => l(:field_fixed_version)),
      :values => lambda {fixed_version_values})
    add_available_filter(
      "issue.category_id",
      :type => :list_optional,
      :name => l("label_attribute_of_issue", :name => l(:field_category)),
      :values => lambda {project.issue_categories.pluck(:name, :id).map {|name, id| [name, id.to_s]}}
    ) if project
    add_available_filter(
      "issue.subject",
      :type => :text,
      :name => l("label_attribute_of_issue", :name => l(:field_subject))
    )
    add_available_filter(
      "user_id",
      :type => :list_optional, :values => lambda {author_values}
    )
    add_available_filter(
      "user.group",
      :type => :list_optional,
      :name => l("label_attribute_of_user", :name => l(:label_group)),
      :values => lambda {Group.givable.visible.pluck(:name, :id).map {|name, id| [name, id.to_s]}}
    )
    add_available_filter(
      "user.role",
      :type => :list_optional,
      :name => l("label_attribute_of_user", :name => l(:field_role)),
      :values => lambda {Role.givable.pluck(:name, :id).map {|name, id| [name, id.to_s]}}
    )
    add_available_filter(
      "author_id",
      :type => :list_optional, :values => lambda {author_values}
    )
    activities = (project ? project.activities : TimeEntryActivity.shared)
    add_available_filter(
      "activity_id",
      :type => :list, :values => activities.map {|a| [a.name, (a.parent_id || a.id).to_s]}
    )
    add_available_filter(
      "project.status",
      :type => :list,
      :name => l(:label_attribute_of_project, :name => l(:field_status)),
      :values => lambda {project_statuses_values}
    ) if project.nil? || !project.leaf?

    add_available_filter "comments", :type => :text
    add_available_filter "hours", :type => :float

    add_custom_fields_filters(time_entry_custom_fields)
    add_associations_custom_fields_filters :project
    add_custom_fields_filters(issue_custom_fields, :issue)
    add_associations_custom_fields_filters :user
  end

  def available_columns
    return @available_columns if @available_columns

    @available_columns = self.class.available_columns.dup
    @available_columns += time_entry_custom_fields.visible.
                            map {|cf| QueryCustomFieldColumn.new(cf)}
    @available_columns += issue_custom_fields.visible.
                            map {|cf| QueryAssociationCustomFieldColumn.new(:issue, cf, :totalable => false)}
    @available_columns += project_custom_fields.visible.
                            map {|cf| QueryAssociationCustomFieldColumn.new(:project, cf)}
    @available_columns
  end

  def default_columns_names
    @default_columns_names ||= begin
      default_columns = Setting.time_entry_list_defaults.symbolize_keys[:column_names].map(&:to_sym)
      project.present? ? default_columns : [:project] | default_columns
    end
  end

  def default_totalable_names
    Setting.time_entry_list_defaults.symbolize_keys[:totalable_names].map(&:to_sym)
  end

  def default_sort_criteria
    [['spent_on', 'desc']]
  end

  # If a filter against a single issue is set, returns its id, otherwise nil.
  def filtered_issue_id
    if value_for('issue_id').to_s =~ /\A(\d+)\z/
      $1
    end
  end

  def base_scope
    scope = TimeEntry.visible
                     .joins(:project, :user)
                     .includes(:activity)
                     .references(:activity)
                     .left_join_issue
                     .where(statement)

    if Redmine::Database.mysql? && ActiveRecord::Base.connection.supports_optimizer_hints?
      # Provides MySQL with a hint to use a better join order and avoid slow response times
      scope.optimizer_hints('JOIN_ORDER(time_entries, projects, users)')
    else
      scope
    end
  end

  def results_scope(options={})
    order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)

    order_option << "#{TimeEntry.table_name}.id ASC"
    base_scope.
      order(order_option).
      joins(joins_for_order_statement(order_option.join(',')))
  end

  # Returns sum of all the spent hours
  def total_for_hours(scope)
    map_total(scope.sum(:hours)) {|t| t.to_f.round(2)}
  end

  def sql_for_issue_id_field(field, operator, value)
    case operator
    when "="
      "#{TimeEntry.table_name}.issue_id = #{value.first.to_i}"
    when "~"
      issue = Issue.where(:id => value.first.to_i).first
      if issue && (issue_ids = issue.self_and_descendants.pluck(:id)).any?
        "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
      else
        "1=0"
      end
    when "!*"
      "#{TimeEntry.table_name}.issue_id IS NULL"
    when "*"
      "#{TimeEntry.table_name}.issue_id IS NOT NULL"
    end
  end

  def sql_for_issue_fixed_version_id_field(field, operator, value)
    issue_ids = Issue.where(:fixed_version_id => value.map(&:to_i)).pluck(:id)
    case operator
    when "="
      if issue_ids.any?
        "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
      else
        "1=0"
      end
    when "!"
      if issue_ids.any?
        "#{TimeEntry.table_name}.issue_id NOT IN (#{issue_ids.join(',')})"
      else
        "1=1"
      end
    end
  end

  def sql_for_issue_parent_id_field(field, operator, value)
    case operator
    when "="
      # accepts a comma separated list of ids
      parent_ids = value.first.to_s.scan(/\d+/).map(&:to_i).uniq
      issue_ids = Issue.where(:parent_id => parent_ids).pluck(:id)
      if issue_ids.present?
        "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
      else
        "1=0"
      end
    when "~"
      root_id, lft, rgt = Issue.where(:id => value.first.to_i).pick(:root_id, :lft, :rgt)
      issue_ids = Issue.where("#{Issue.table_name}.root_id = ? AND #{Issue.table_name}.lft > ? AND #{Issue.table_name}.rgt < ?", root_id, lft, rgt).pluck(:id) if root_id && lft && rgt
      if issue_ids.present?
        "#{TimeEntry.table_name}.issue_id IN (#{issue_ids.join(',')})"
      else
        "1=0"
      end
    else
      sql_for_field("parent_id", operator, value, Issue.table_name, "parent_id")
    end
  end

  def sql_for_activity_id_field(field, operator, value)
    ids = value.map(&:to_i).join(',')
    table_name = Enumeration.table_name
    if operator == '='
      "(#{table_name}.id IN (#{ids}) OR #{table_name}.parent_id IN (#{ids}))"
    else
      "(#{table_name}.id NOT IN (#{ids}) AND (#{table_name}.parent_id IS NULL OR #{table_name}.parent_id NOT IN (#{ids})))"
    end
  end

  def sql_for_issue_tracker_id_field(field, operator, value)
    sql_for_field("tracker_id", operator, value, Issue.table_name, "tracker_id")
  end

  def sql_for_issue_status_id_field(field, operator, value)
    sql_for_field("status_id", operator, value, Issue.table_name, "status_id")
  end

  def sql_for_issue_category_id_field(field, operator, value)
    sql_for_field("category_id", operator, value, Issue.table_name, "category_id")
  end

  def sql_for_issue_subject_field(field, operator, value)
    sql_for_field("subject", operator, value, Issue.table_name, "subject")
  end

  def sql_for_project_status_field(field, operator, value, options={})
    sql_for_field(field, operator, value, Project.table_name, "status")
  end

  def sql_for_user_group_field(field, operator, value)
    if operator == '*' # Any group
      groups = Group.givable
      operator = '='
    elsif operator == '!*'
      groups = Group.givable
      operator = '!'
    else
      groups = Group.where(:id => value).to_a
    end
    groups ||= []

    members_of_groups = groups.inject([]) do |user_ids, group|
      user_ids + group.user_ids
    end.uniq.compact.sort.collect(&:to_s)

    '(' + sql_for_field('user_id', operator, members_of_groups, TimeEntry.table_name, "user_id", false) + ')'
  end

  def sql_for_user_role_field(field, operator, value)
    case operator
    when "*", "!*"
      sw = operator == "!*" ? "NOT" : ""
      nl = operator == "!*" ? "#{TimeEntry.table_name}.user_id IS NULL OR" : ""

      subquery =
        "SELECT 1" +
        " FROM #{Member.table_name}" +
        " WHERE #{TimeEntry.table_name}.project_id = #{Member.table_name}.project_id AND #{Member.table_name}.user_id = #{TimeEntry.table_name}.user_id"
      "(#{nl} #{sw} EXISTS (#{subquery}))"
    when "=", "!"
      role_cond =
        if value.any?
          "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
        else
          "1=0"
        end
      sw = operator == "!" ? 'NOT' : ''
      nl = operator == "!" ? "#{TimeEntry.table_name}.user_id IS NULL OR" : ''
      subquery =
        "SELECT 1" +
        " FROM #{Member.table_name} inner join #{MemberRole.table_name} on members.id = member_roles.member_id" +
        " WHERE #{TimeEntry.table_name}.project_id = #{Member.table_name}.project_id AND #{Member.table_name}.user_id = #{TimeEntry.table_name}.user_id AND #{role_cond}"
      "(#{nl} #{sw} EXISTS (#{subquery}))"
    end
  end

  # Accepts :from/:to params as shortcut filters
  def build_from_params(params, defaults={})
    super
    if params[:from].present? && params[:to].present?
      add_filter('spent_on', '><', [params[:from], params[:to]])
    elsif params[:from].present?
      add_filter('spent_on', '>=', [params[:from]])
    elsif params[:to].present?
      add_filter('spent_on', '<=', [params[:to]])
    end
    self
  end

  def joins_for_order_statement(order_options)
    joins = [super]

    if order_options
      if order_options.include?('issue_statuses')
        joins << "LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id"
      end
      if order_options.include?('trackers')
        joins << "LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id"
      end
      if order_options.include?('issue_categories')
        joins << "LEFT OUTER JOIN #{IssueCategory.table_name} ON #{IssueCategory.table_name}.id = #{Issue.table_name}.category_id"
      end
      if order_options.include?('versions')
        joins << "LEFT OUTER JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Issue.table_name}.fixed_version_id"
      end
    end

    joins.compact!
    joins.any? ? joins.join(' ') : nil
  end
end
