require 'easy_extensions/easy_mail_template_issue'

module EasyPatch
  module IssuePatch

    def self.included(base)
      base.extend(ClassMethods)
      base.send(:include, InstanceMethods)
      base.send(:include, ActionView::Helpers::DateHelper)
      base.send(:include, ActionView::Helpers::TagHelper)

      base.class_eval do

        belongs_to :activity, :class_name => 'TimeEntryActivity'
        belongs_to :easy_closed_by, :class_name => 'User'
        belongs_to :easy_last_updated_by, :class_name => 'User'
        has_many :easy_issue_timers, :dependent => :destroy
        has_many :easy_favorites, :as => :entity
        has_many :favorited_by, lambda { uniq }, :through => :easy_favorites, :source => :user, :dependent => :destroy
        has_many :easy_entity_activities, :as => :entity, :dependent => :destroy
        # it is reordered by class method
        has_many :used_in_repositories, lambda { uniq }, :through => :changesets, :source => :repository
        has_many :related_easy_entity_assignments, as: :entity_to, class_name: 'EasyEntityAssignment'

        has_one :easy_report_issue_status, :dependent => :destroy

        scope :non_templates, lambda { joins(:project).where(:projects => {:easy_is_easy_template => false}) }
        scope :templates, lambda { joins(:project).where(:projects => {:easy_is_easy_template => true}) }

        scope :opened, lambda { joins(:status).where({IssueStatus.table_name => {:is_closed => false}}) }
        scope :closed, lambda { joins(:status).where({IssueStatus.table_name => {:is_closed => true}}) }
        scope :overdue, lambda { where("(#{self.table_name}.easy_due_date_time IS NOT NULL AND #{self.table_name}.easy_due_date_time < ?) OR (#{self.table_name}.due_date IS NOT NULL AND #{self.table_name}.due_date < ?)", Time.now, Date.today) }

        scope :like, lambda { |*args| where(search_tokens_condition([:subject, :id].map { |n| "#{self.table_name}.#{n}" }, Array(args).reject(&:blank?), false)) }

        scope :visible, lambda {|*args|
          options = args.extract_options!
          user = args.shift || User.current
          options[:skip_pre_condition] = true
          joins(project: :enabled_modules).where(Issue.visible_condition(user, options)).where(enabled_modules: {name: 'issue_tracking'})
        }

        html_fragment :description, :scrub => :strip

        searchable_options[:scope] = lambda { |options| options[:open_issues] ? self.non_templates.open : self.non_templates.all }
        searchable_options[:title_columns] = ['subject', "#{table_name}.id"]
        searchable_options[:preload] << :attachments << :journals << { tracker: :custom_fields } << { custom_values: :custom_field }

        event_options[:title] = Proc.new do |issue|
          s = ''
          s << "##{issue.id}: " if EasySetting.value('show_issue_id')
          s + "#{issue.tracker}: #{Sanitize.clean(issue.subject || '', :output => :html)}"
        end

        acts_as_taggable_on :tags

        acts_as_easy_journalized :non_journalized_columns => %w(root_id easy_level easy_external_id easy_last_updated_by_id
                                 easy_repeat_settings easy_next_start easy_status_updated_on easy_reopen_at easy_closed_by_id closed_on easy_due_date_time),
                                 :format_detail_boolean_columns => ['easy_is_repeating'],
                                 :format_detail_time_columns => [],
                                 :format_detail_reflection_columns => [],
                                 :important_columns => ['assigned_to_id', 'priority_id', 'status_id']

        include EasyPatch::Acts::Repeatable
        acts_as_easy_repeatable

        acts_as_easy_entity_replacable_tokens :easy_query_class => EasyIssueQuery, :token_prefix => 'task'

        acts_as_user_readable

        set_associated_query_class EasyIssueQuery

        # set scope default activities(sidebar)
        # easy_activity_options[:type][:user_scope] => Proc.new { |user, scope| scope.where ... }
        # :type => 'issues' .. determines for which type the options is, Journal is for multiple types
        self.activity_provider_options[:easy_activity_options] = {
          easy_event_type_name => {
            :user_scope => Proc.new { |user, scope| scope.joins("LEFT JOIN #{Watcher.table_name} ON #{Watcher.table_name}.watchable_type='Issue' AND #{Watcher.table_name}.watchable_id=#{Issue.table_name}.id").where("#{Watcher.table_name}.user_id = ? OR #{Issue.table_name}.author_id = ? OR #{Issue.table_name}.assigned_to_id = ?", user.id, user.id, user.id) }
          }
        }
        self.activity_provider_options[:update_timestamp] = "#{table_name}.updated_on"

        include EasyExtensions::EasyInlineFragmentStripper
        strip_inline_images :description

        delegate :easy_type, :easy_type=, :private_easy_type, :private_easy_type=, :to => :current_journal, :allow_nil => true

        attr_reader :issue_move_to_project_errors, :copied_from, :copied_issue_ids
        attr_accessor :relation, :mass_operations_in_progress, :send_to_external_mails, :attributes_for_descendants, :update_repeat_entity_attributes, :copy_notes_to_parent, :without_notifications, :notification_sent
        attr_reader :should_send_invitation_update

        delete_safe_attribute 'custom_field_values'
        safe_attributes 'author_id', 'custom_field_values',
                        :if => lambda { |issue, user| issue.new_record? || issue.attributes_editable?(user) }
        safe_attributes 'activity_id',
                        :if => lambda { |issue, user| issue.project && issue.project.fixed_activity? }

        delete_safe_attribute 'watcher_user_ids'
        safe_attributes 'watcher_user_ids', 'watcher_group_ids',
                        :if => lambda { |issue, user| user.allowed_to?(:add_issue_watchers, issue.project) }

        delete_safe_attribute 'estimated_hours'
        safe_attributes 'estimated_hours', :if => lambda { |issue, user| issue.attributes_editable?(user) && user.allowed_to?(:view_estimated_hours, issue.project) }

        safe_attributes 'parent_issue_id',
                        :if => lambda { |issue, user| issue.attributes_editable?(user) && user.allowed_to?(:manage_subtasks, issue.project) }

        safe_attributes 'easy_type', 'easy_distributed_tasks'

        safe_attributes 'easy_due_date_time', 'easy_start_date_time', 'send_to_external_mails', 'relation', 'update_repeat_entity_attributes', 'copy_notes_to_parent', 'without_notifications'

        safe_attributes 'project_id', 'tracker_id', 'status_id', 'category_id', 'assigned_to_id',
                        'priority_id', 'fixed_version_id', 'subject', 'description', 'start_date', 'due_date', 'done_ratio',
                        'custom_field_values', 'custom_fields', 'lock_version', 'notes', 'tag_list', :if => lambda { |issue, user| issue.attributes_editable?(user) }

        accepts_nested_attributes_for :easy_entity_activities, reject_if: :all_blank, allow_destroy: true

        before_validation :create_issue_relations

        validates :activity_id, :presence => true, :if => Proc.new { |issue| issue.project && issue.project.fixed_activity? }
        validates :is_private, :inclusion => {:in => [true, false]}
        validate :validate_do_not_allow_close_if_subtasks_opened
        validate :validate_do_not_allow_close_if_no_attachments
        validate :validate_easy_distributed_task, :if => Proc.new { |issue| issue.tracker && issue.tracker.easy_distributed_tasks? }

        before_save :set_percent_done, :update_easy_closed_by
        before_save :update_easy_status_updated_on, :if => Proc.new { |issue| issue.status_id_changed? || issue.new_record? }
        before_save :set_easy_last_updated_by_id

        after_update :remove_watchers, :if => Proc.new { |issue| issue.project_id_changed? }
        after_save :move_fixed_version_effective_date_if_needed
        after_save :close_children, :if => Proc.new { |issue| EasySetting.value(:close_subtask_after_parent) }
        after_save :save_easy_distributed_tasks
        after_save :copy_notes_to_parent_task, :if => Proc.new { |issue| issue.copy_notes_to_parent && EasySetting.value(:issue_copy_notes_to_parent, issue.project_id) }
        after_save :journal_to_parent_task_if_child_changed, :if => Proc.new { |issue| issue.parent_id_changed? }
        after_save :set_notify_descendants, :if => Proc.new { |issue| issue.root? && EasySetting.value(:issue_copy_notes_to_parent, issue.project_id) }
        after_commit :send_notification, on: :create

        # Issue must be commited because of delay send
        skip_callback :create, :after, :send_notification

        alias_method_chain :status=, :easy_extensions
        alias_method_chain :after_create_from_copy, :easy_extensions
        alias_method_chain :assignable_users, :easy_extensions
        alias_method_chain :cache_key, :easy_extensions
        alias_method_chain :css_classes, :easy_extensions
        alias_method_chain :copy_from, :easy_extensions
        alias_method_chain :editable?, :easy_extensions
        alias_method_chain :deletable?, :easy_extensions
        alias_method_chain :attributes_editable?, :easy_extensions
        alias_method_chain :estimated_hours=, :easy_extensions
        alias_method_chain :journalized_attribute_names, :easy_extensions
        alias_method_chain :new_statuses_allowed_to, :easy_extensions
        alias_method_chain :notified_users, :easy_extensions
        alias_method_chain :overdue?, :easy_extensions
        alias_method_chain :read_only_attribute_names, :easy_extensions
        alias_method_chain :recalculate_attributes_for, :easy_extensions
        alias_method_chain :required_attribute_names, :easy_extensions
        alias_method_chain :relations, :easy_extensions
        alias_method_chain :reschedule_on!, :easy_extensions
        alias_method_chain :safe_attributes=, :easy_extensions
        alias_method_chain :send_notification, :easy_extensions
        alias_method_chain :to_s, :easy_extensions
        alias_method_chain :validate_issue, :easy_extensions
        alias_method_chain :after_project_change, :easy_extensions
        alias_method_chain :visible?, :easy_extensions
        alias_method_chain :workflow_rule_by_attribute, :easy_extensions
        alias_method_chain :available_custom_fields, :easy_extensions
        alias_method_chain :reload, :easy_extensions

        class << self

          alias_method_chain :count_and_group_by, :easy_extensions
          alias_method_chain :cross_project_scope, :easy_extensions
          alias_method_chain :search_result_ranks_and_ids, :easy_extensions
          alias_method_chain :search_token_match_statement, :easy_extensions
          alias_method_chain :self_and_descendants, :easy_extensions
          alias_method_chain :visible_condition, :easy_extensions

          def by_custom_field(cf, project)
            count_and_group_by_custom_field(cf, {:project => project})
          end

          def by_custom_fields(project)
            reported_cf = Hash.new

            cfs = IssueCustomField.where(:is_for_all => true, :field_format => EasyExtensions.reportable_issue_cfs)
            cfs += project.issue_custom_fields.where(:field_format => EasyExtensions.reportable_issue_cfs)
            cfs.uniq!

            cfs.each_with_index do |i, index|
              data = {
                :reports => by_custom_field(i, project),
                :name => i.name
              }
              reported_cf["cf_#{i.id}"] = data
            end

            return reported_cf
          end

          def by_unassigned_to(project)
            Issue.connection.select_all("SELECT
           s.id AS status_id,
           s.is_closed AS closed,
           NULL AS assigned_to_id,
           count(issues.id)AS total
           FROM
           #{Issue.table_name},
           #{Project.table_name},
           #{IssueStatus.table_name} s
           WHERE
           #{Issue.table_name}.status_id = s.id
           AND #{Issue.table_name}.assigned_to_id IS NULL
           AND #{Issue.table_name}.project_id = #{Project.table_name}.id
           and #{visible_condition(User.current, :project => project)}
           GROUP BY
           s.id,
           s.is_closed")
          end

          def update_from_gantt(data)
            unsaved_issues = []
            unsaved_versions = []
            possible_unsaved_issue = nil
            possible_unsaved_version = nil
            (data['projects']['project']['task'].kind_of?(Array) ? data['projects']['project']['task'] : [data['projects']['project']['task']]).each do |gantt_data|
              if gantt_data['childtasks']
                # milestone
                possible_unsaved_version = Version.update_version_from_gantt_data(gantt_data)
                unsaved_versions << possible_unsaved_version if possible_unsaved_version
                (gantt_data['childtasks']['task'].kind_of?(Array) ? gantt_data['childtasks']['task'] : [gantt_data['childtasks']['task']]).each do |child_data|
                  possible_unsaved_issue = self.update_issue_from_gantt_data(child_data)
                  unsaved_issues << possible_unsaved_issue if possible_unsaved_issue
                end
              else
                possible_unsaved_issue = self.update_issue_from_gantt_data(gantt_data)
                unsaved_issues << possible_unsaved_issue if possible_unsaved_issue
              end
            end
            {:unsaved_issues => unsaved_issues, :unsaved_versions => unsaved_versions}
          end

          def parse_gantt_date(date_string)
            if date_string.match('\d{4},\d{1,2},\d{1,2}')
              Date.strptime(date_string, '%Y,%m,%d')
            end
          end

          def count_and_group_by_custom_field(cf, options)
            project = options.delete(:project)
            ActiveRecord::Base.connection.select_all("
              SELECT s.id as status_id, s.is_closed as closed, j.value as cf_#{cf.id}, count(#{Issue.table_name}.id) as total
              FROM #{Issue.table_name}
              JOIN #{CustomValue.table_name} j ON j.customized_id = #{Issue.table_name}.id AND j.customized_type = 'Issue'
              JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Issue.table_name}.project_id
              JOIN #{IssueStatus.table_name} s ON s.id = #{Issue.table_name}.status_id
              WHERE
              j.custom_field_id = #{cf.id}
              AND #{Issue.visible_condition(User.current, :project => project)}
              GROUP BY s.id, s.is_closed, j.value
              ")
          end


          # used in workflow_rule_by_attribute as cache of fields_with_roles
          def non_visible_custom_field_with_roles
            RequestStore.store['issue_custom_fields_by_role'] ||=
              IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, 'role_id').inject({}) do |memo, (cf_id, role_id)|
                memo[cf_id] ||= []
                memo[cf_id] << role_id
                memo
              end
          end

          def css_icon
            'icon icon-tracker'
          end

          # loads available custom field ids to cache
          # it needs two sql queries.
          # first query load custom_fields for all projects
          # second query load custom_fields, wich are not for all projects, if project_ids is given, than this query is limited only for those projects
          # both queries are categorized by tracker_id
          # the result in format {<project_id1> => {<tracker_id1>=>[<cf_id1>, <cf_id2>]}} is saved to request store
          # then all issue custom fields are retrieved to array.
          # if available custom_fields are requested for projec and tracker, only we need to do is retrieve result[project.id][tracker.id] ids and map them to custom fields.
          # TODO: wouldn't be bit faster first query per tracker second for projects and than & on retrieve?
          def load_available_custom_fields_cache(project_ids=nil)
            RequestStore.store['issue_available_custom_fields_loaded_for'] ||= []
            return if project_ids && project_ids.empty?
            return if project_ids && (project_ids_to_load = ((project_ids || []) - RequestStore.store['issue_available_custom_fields_loaded_for'])).any?
            if project_ids.nil? || RequestStore.store['issue_available_custom_fields'].nil?
              q =  "SELECT cft.tracker_id, #{CustomField.quoted_table_name}.id "
              q << "FROM #{CustomField.quoted_table_name} "
              q << "  INNER JOIN #{table_name_prefix}custom_fields_trackers#{table_name_suffix} cft ON cft.custom_field_id = #{CustomField.quoted_table_name}.id "
              q << "WHERE #{CustomField.quoted_table_name}.type IN ('IssueCustomField') AND #{CustomField.quoted_table_name}.disabled = #{CustomField.connection.quoted_false}"
              q << "  AND #{CustomField.quoted_table_name}.is_for_all = #{CustomField.connection.quoted_true}"
              for_all = CustomField.connection.select_rows(q).each_with_object({}) do |row, res|
                res[row.first.to_i] ||= []
                res[row.first.to_i]  << row.second.to_i
              end
              RequestStore.store['issue_available_custom_fields'] = Hash.new{|h, k| h[k] = for_all.deep_dup }
            end

            q =  "SELECT cfp.project_id, cft.tracker_id, #{CustomField.quoted_table_name}.id "
            q << "FROM #{CustomField.quoted_table_name} "
            q << "  INNER JOIN #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp ON cfp.custom_field_id = #{CustomField.quoted_table_name}.id "
            q << "  INNER JOIN #{table_name_prefix}custom_fields_trackers#{table_name_suffix} cft ON cft.custom_field_id = #{CustomField.quoted_table_name}.id "
            q << "WHERE #{CustomField.quoted_table_name}.type IN ('IssueCustomField') AND #{CustomField.quoted_table_name}.disabled = #{CustomField.connection.quoted_false} "
            q << "  AND #{CustomField.quoted_table_name}.is_for_all = #{CustomField.connection.quoted_false} "
            q << "  AND cfp.project_id IN (#{(project_ids_to_load).join(', ')})" if project_ids_to_load
            CustomField.connection.select_rows(q).each_with_object(RequestStore.store['issue_available_custom_fields']) do |row, cache|
              cache[row.first.to_i][row.second.to_i] ||= []
              cache[row.first.to_i][row.second.to_i]  << row.last.to_i
            end

            RequestStore.store['all_issue_custom_fields_by_id'] ||= IssueCustomField.with_group.sorted.to_a
            RequestStore.store['issue_available_custom_fields_loaded_for'] |= project_ids ? project_ids : Project.all.pluck(:id)
          end

          def available_custom_fields_cache
            RequestStore.store['issue_available_custom_fields']
          end

          def available_custom_fields_from_cache( project_id, tracker_id )
            if RequestStore.store['issue_available_custom_fields'] && (!RequestStore.store['issue_available_custom_fields_loaded_for'] || RequestStore.store['issue_available_custom_fields_loaded_for'].include?(project_id))
              ids = RequestStore.store['issue_available_custom_fields'][project_id][tracker_id]
              ids && RequestStore.store['all_issue_custom_fields_by_id'].select{|cf| ids.include?(cf.id) } || []
            end
          end

        end


        # reorders association
        def used_in_repositories
          super.reorder(nil)
        end

        def parent_issue
          @parent_issue
        end

        def parent_project
          @parent_project ||= self.project.parent_project if self.project
        end

        def parent_category
          @parent_category ||= self.category.parent if self.category
        end

        def root_category
          @root_category ||= self.category.root if self.category
        end

        def main_project
          @main_project ||= self.project.main_project if self.project
        end

        def remaining_timeentries
          @remaining_timeentries ||= ((self.estimated_hours || 0.0) - self.spent_hours)
        end

        def total_remaining_timeentries
          @total_remaining_timeentries ||= ((self.total_estimated_hours || 0.0) - self.total_spent_hours)
        end

        def spent_estimated_timeentries
          @spent_estimated_timeentries ||= begin
            if self.estimated_hours && self.estimated_hours > 0
              ((self.spent_hours / self.estimated_hours) * 100).to_i
            else
              0.0
            end
          end
        end

        def total_spent_estimated_timeentries
          @total_spent_estimated_timeentries ||= begin
            if self.total_estimated_hours && self.total_estimated_hours > 0
              ((self.total_spent_hours / self.total_estimated_hours) * 100).to_i
            else
              0.0
            end
          end
        end

        def open_duration_in_hours
          self.closed_on - self.created_on if self.closed_on
        end

        def status_time_current
          (Time.now - self.easy_status_updated_on) / 1.minute if self.easy_status_updated_on
        end

        def last_user_assigned_to
          return @last_user_assigned_to unless @last_user_assigned_to.nil?

          t = Journal.arel_table
          journals_detail = JournalDetail.joins(:journal).where(t[:journalized_type].eq(self.class.base_class.name).and(t[:journalized_id].eq(self.id))).where(:prop_key => 'assigned_to_id').order(t[:created_on].desc.to_sql).first

          if journals_detail
            last_assigned_to = Principal.where(:id => journals_detail.old_value).last if journals_detail.old_value
          else
            last_assigned_to = self.assigned_to
          end
          @last_user_assigned_to = last_assigned_to
        end

        def build_issue_relations_from_params(params)
          if params && params['issue_to_id']
            Issue.where(:id => params['issue_to_id']).each do |issue_to|
              relations_from.build(:relation_type => params['relation_type'], :delay => params['relation_delay'], :issue_from => self, :issue_to => issue_to)
            end
          end
        end

        def to_s_with_id
          suffix = self.easy_is_repeating? ? (' ' << l(:label_easy_issue_subject_reccuring_suffix)) : ''
          "##{self.id} - #{self.subject}#{suffix}"
        end

        def to_s_without_id
          suffix = self.easy_is_repeating? ? (' ' << l(:label_easy_issue_subject_reccuring_suffix)) : ''
          "#{self.subject}#{suffix}"
        end

        def has_relations_to_copy?(with_descendants = false)
          issues = with_descendants ? self.self_and_descendants.visible.reorder(nil) : [self]
          Issue.load_relations(issues)
          issues.each do |issue|
            issue.relations.each do |relation|
              unless IssueRelation::NOT_COPIED_RELATIONS.include?(relation.relation_type)
                return true
              end
            end
          end
          false
        end

        def get_notified_users_for_mail(type = :all, journal = nil)
          if self.assigned_to
            assigned_notified_users = (self.assigned_to.is_a?(Group) ? self.assigned_to.users : [self.assigned_to]).select { |u| u.active? && u.notify_about?(self) }
          else
            assigned_notified_users = []
          end
          if journal
            assigned_notified_users = (assigned_notified_users & journal.notified_users)
          end

          case type
          when :all
            issue_notified_users = journal.nil? ? self.notified_users : journal.notified_users
            issue_notified_watchers = (journal.nil? ? self.notified_watchers : journal.notified_watchers) - issue_notified_users
          when :assigned_to
            issue_notified_users = assigned_notified_users
            issue_notified_watchers = []
          when :except_assigned_to
            issue_notified_users = []
            if journal
              issue_notified_watchers = (journal.notified_users | journal.notified_watchers) - assigned_notified_users
            else
              issue_notified_watchers = (self.notified_users | self.notified_watchers) - assigned_notified_users
            end
          end

          if journal && journal.private_notes?
            issue_notified_users.reject! { |user| !user.allowed_to?(:view_private_notes, project) }
            issue_notified_watchers.reject! { |user| !user.allowed_to?(:view_private_notes, project) }
          end

          {:to => issue_notified_users, :cc => issue_notified_watchers}
        end

        def get_notified_mails_for_mail(type = :all, journal = nil)
          users = get_notified_users_for_mail(type, journal)
          {:to => users[:to].collect(&:notify_mails).flatten, :cc => users[:cc].collect(&:notify_mails).flatten}
        end

        def get_notified_users_for_mail_grouped_by_lang(type = :all, journal = nil)
          users_to_notify = self.get_notified_users_for_mail(type, journal)

          users_to_notify_to = users_to_notify[:to]
          users_to_notify_cc = users_to_notify[:cc]

          users_to_notify_to_by_lang = users_to_notify_to.inject({}) { |memo, u| memo[u.language] ||= []; memo[u.language].concat u.notify_mails; memo }
          users_to_notify_cc_by_lang = users_to_notify_cc.inject({}) { |memo, u| memo[u.language] ||= []; memo[u.language].concat u.notify_mails; memo }

          {:to => users_to_notify_to_by_lang, :cc => users_to_notify_cc_by_lang}
        end

        def get_easy_mail_template
          EasyExtensions::EasyMailTemplateIssue
        end

        def get_status_time(id)
          self.easy_report_issue_status.get_status_time(self.easy_report_issue_status.get_idx(id)) if id && self.easy_report_issue_status
        end

        def get_status_count(id)
          self.easy_report_issue_status.get_status_count(self.easy_report_issue_status.get_idx(id)) if id && self.easy_report_issue_status
        end

        def easy_distributed_tasks
          @easy_distributed_tasks.blank? ? [{}] : @easy_distributed_tasks
        end

        def easy_distributed_tasks=(tasks)
          return if !self.new_record? || self.tracker.nil? || !self.tracker.easy_distributed_tasks?
          if tasks.is_a?(Hash) && tasks.has_key?(:assigned_to_ids)
            @easy_distributed_tasks = []
            tasks[:assigned_to_ids].each_with_index do |assigned_to_id, i|
              @easy_distributed_tasks << {
                :assigned_to_id => assigned_to_id,
                :est => tasks[:ests].try(:at, i),
              }
            end
          else
            @easy_distributed_tasks = nil
          end
          self.build_easy_distributed_tasks
        end

        def easy_due_date_time_remaining
          if time_value = easy_due_date_time.try(:to_time)
            (time_value - Time.now) / 1.hour.to_f
          end
          time_value
        end

        def easy_merge_to(issue_to_merge, status)
          #
          # selected entities to copy
          # for example: related entities is not necessary to copy
          #
          entities_types_to_copy = {'Journal' => 1, 'Attachment' => 1}

          issue_to_merge.reload
          # merge custom values
          custom_values.each do |v|
            easy_merge_custom_value(issue_to_merge, v)
          end
          Mailer.with_deliveries(false) do
            issue_to_merge.save
          end

          associations_to_merge = [:attachments, :relations_from, :relations_to, :journals]

          associations_to_merge.each do |association|
            assoc = association.to_s
            reflection = Issue.reflections[assoc]

            case reflection.macro
            when :has_and_belongs_to_many, :has_many
              entities = self.send(assoc)
              next if entities.blank?

              entities.each do |r|
                duplicate = easy_duplicate_entity_for_merge(r, entities_types_to_copy)
                begin
                  Mailer.with_deliveries(false) do
                    issue_to_merge.send("#{assoc}").send('<<', duplicate)
                  end
                rescue StandardError => e
                  # association already contains duplicate object
                  # read only associations
                end
              end
            end
          end

          self.reload
          self.status = status
          Mailer.with_deliveries(false) do
            self.save
          end
        end

        def easy_duplicate_entity_for_merge(original, entities_types_to_copy)
          duplicate = original
          if entities_types_to_copy[original.class.name]
            begin
              duplicate = original.dup
            rescue StandardError => e
              # cannot duplicate object, use original instead
            end
          end
          duplicate
        end

        def easy_merge_custom_value(original_issue, custom_value_to_merge)
          case custom_value_to_merge.custom_field.format
          when EasyExtensions::FieldFormats::Email, Redmine::FieldFormat::StringFormat, Redmine::FieldFormat::TextFormat
            easy_merge_text_custom_value(original_issue, custom_value_to_merge, ',')
          end
        end

        def easy_merge_text_custom_value(original_issue, custom_value_to_merge, separator = nil)
          original_cv = original_issue.custom_value_for(custom_value_to_merge.custom_field_id)
          if (original_cv)
            if separator
              new_value = (original_cv.value.to_s.split(separator) + custom_value_to_merge.value.to_s.split(separator)).uniq.join(separator)
            else
              new_value = original_cv.value.to_s + custom_value_to_merge.value.to_s
            end

            original_issue.custom_field_values = {custom_value_to_merge.custom_field.id.to_s => new_value.to_s}
          else
            original_issue.custom_field_values = {custom_value_to_merge.custom_field.id.to_s => custom_value_to_merge.value.to_s}
          end
        end

        def build_easy_distributed_tasks
          @easy_distributed_tasks_to_save = []
          self.easy_distributed_tasks.each do |data|
            self.build_easy_distributed_task(data) if data[:assigned_to_id].present? && data[:est].present?
          end
        end

        def build_easy_distributed_task(data)
          task = Issue.new
          task.attributes = self.attributes.dup.slice('project_id',
                                                      'author_id', 'activity_id', 'priority_id', 'description', 'status_id', 'start_date', 'due_date')

          task.tracker = distributed_tracker if self.project
          assigned_to = User.find_by_id(data[:assigned_to_id])
          task.subject = self.subject.dup << (assigned_to ? (' ' << assigned_to.login) : '')
          task.estimated_hours = data[:est]
          task.assigned_to_id = data[:assigned_to_id]
          @easy_distributed_tasks_to_save << task
        end

        def is_favorited?(user = nil)
          user ||= User.current

          self.easy_favorites.where(:user_id => user.id).exists?
        end

        def save_easy_distributed_tasks
          if @easy_distributed_tasks_to_save
            @easy_distributed_tasks_to_save.each do |st|
              st.custom_values = self.custom_values.map { |v| cloned_v = v.dup; cloned_v.customized = st; cloned_v }
              st.parent_issue_id = self.id
              st.save
            end
            @easy_distributed_tasks_to_save = nil
          end
        end

        def close_children
          if self.closed? && self.children.any?
            self.descendants.update_all(:status_id => self.status_id)
          end
        end

        def update_easy_closed_by
          if closing? || (new_record? && closed?)
            self.easy_closed_by = User.current
          end
        end

        def update_easy_status_updated_on
          self.easy_status_updated_on = Time.now
        end

        def create_issue_relations
          build_issue_relations_from_params(relation) if new_record?
        end

        def validate_do_not_allow_close_if_subtasks_opened
          return if self.tracker.nil? || !self.tracker.respond_to?(:easy_do_not_allow_close_if_subtasks_opened)
          return if self.leaf? || !self.status || !self.status.is_closed?
          return unless self.tracker.easy_do_not_allow_close_if_subtasks_opened?

          unclosed = self.descendants.joins(:status).where(["#{IssueStatus.table_name}.is_closed = ?", false])

          return unless unclosed.exists?

          errors.add :base, l(:error_cannot_close_issue_due_to_subtasks, :issues => ('<br>' + unclosed.to_a.collect { |i| i.to_s }.join('<br>'))).html_safe
        end

        def validate_do_not_allow_close_if_no_attachments
          return if !self.tracker || !self.tracker.respond_to?(:easy_do_not_allow_close_if_no_attachments)
          return if !self.status || !self.status.is_closed?
          return unless self.tracker.easy_do_not_allow_close_if_no_attachments?
          return if self.attachments.size > 0

          errors.add :base, l(:error_cannot_close_issue_due_to_no_attachments)
        end

        def validate_easy_distributed_task
          if distributed_tracker
            errors.add :base, l(:error_parent_issue_id_is_disabled) if distributed_tracker.disabled_core_fields.include?('parent_issue_id')
          else
            errors.add :base, l(:error_cannot_create_distributed_tasks_without_tracker)
          end
        end

        def distributed_tracker
          @distributed_tracker ||= self.project.trackers.where(:easy_distributed_tasks => false).first if self.project
        end

        def remove_watchers #removes watchers if user is not a member of new project
          self.watcher_users = (self.project.users & self.watcher_users)
        end

        def move_fixed_version_effective_date_if_needed
          if EasySetting.value('milestone_effective_date_from_issue_due_date') && self.fixed_version && self.fixed_version.effective_date && self.due_date
            if self.fixed_version.effective_date < self.due_date
              journal = self.fixed_version.init_journal(User.current, l(:text_milestone_effective_date_from_issue, :issue => self.id))
              self.fixed_version.update_attributes(:effective_date => self.due_date)
            end
          end
        end

        def set_percent_done
          return if !EasySetting.value('issue_set_done_after_close')
          return if self.done_ratio == 100

          if self.status_id_changed? && self.status && self.status.is_closed?
            self.done_ratio = 100
          end
        end

        def easy_journal_option(option, journal)
          case option
          when :title
            new_status = journal.new_status
            "#{journal.issue.tracker} ##{journal.issue.id}#{new_status ? " (#{new_status})" : nil}: #{journal.issue.subject}"
          when :type
            new_status = journal.new_status
            if new_status
              new_status.is_closed? ? 'issue-closed' : 'issue-edit'
            else
              'issue-note'
            end
          when :url
            {:controller => 'issues', :action => 'show', :id => journal.issue.id, :anchor => "change-#{journal.id}"}
          end
        end

        def easy_journal_event_group
          :issue
        end

        def copy_notes_to_parent_task
          if @current_journal && parent = self.parent
            parent.init_journal(@current_journal.user, @current_journal.notes)
            parent.current_journal.private_notes = @current_journal.private_notes
            parent.current_journal.notify_children = true
            if parent.save
              @current_journal.notify = false
            else
              self.errors.messages.merge!(parent.errors.messages)
              self.errors.add(:base, l(:error_copy_notes_to_parent))
            end
          end
        end

        def journal_to_parent_task_if_child_changed
          if self.parent_id_was && (old_parent = Issue.find_by(id: self.parent_id_was))
            journal = old_parent.init_journal(User.current)
            journal.details << JournalDetail.new(property: 'relation', prop_key: 'subtask', old_value: self.id, value: nil)
            journal.save
          end
          if (new_parent = self.parent)
            journal = new_parent.init_journal(User.current)
            journal.details << JournalDetail.new(property: 'relation', prop_key: 'subtask', old_value: nil, value: self.id)
            journal.save
          end
        end

        def set_easy_last_updated_by_id
          self.easy_last_updated_by_id = current_journal.try(:user_id) || User.current.id
        end

        def easy_divided_hours
          @easy_divided_hours ||= time_entries.sum(:easy_divided_hours) || 0
        end

        def set_notify_descendants
          @current_journal.notify_children = true if @current_journal
        end

      end
    end

    module ClassMethods

      def search_result_ranks_and_ids_with_easy_extensions(tokens, user=User.current, projects=nil, options={})
        tokens = [] << tokens unless tokens.is_a?(Array)
        projects = [] << projects if projects.is_a?(Project)

        columns = searchable_options[:columns]
        columns = searchable_options[:title_columns] || columns[0..0] if options[:titles_only]

        r = []
        queries = 0

        unless options[:attachments] == 'only'
          r = fetch_ranks_and_ids(
            search_scope(user, projects, options).
              where(search_tokens_condition(columns, tokens, options[:all_words])),
            options[:limit]
          )
          queries += 1

          if !options[:titles_only] && searchable_options[:search_custom_fields]
            searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a

            if searchable_custom_fields.any?
              fields_by_visibility = searchable_custom_fields.group_by { |field|
                field.visibility_by_project_condition(searchable_options[:project_key], user, "#{CustomValue.table_name}.custom_field_id")
              }
              clauses = []
              fields_by_visibility.each do |visibility, fields|
                clauses << "(#{CustomValue.table_name}.custom_field_id IN (#{fields.map(&:id).join(',')}) AND (#{visibility}))"
              end
              visibility = clauses.join(' OR ')

              r |= fetch_ranks_and_ids(
                search_scope(user, projects, options).
                  joins(:custom_values).
                  where(visibility).
                  where(search_tokens_condition(["#{CustomValue.table_name}.value"], tokens, options[:all_words])),
                options[:limit]
              )
              queries += 1
            end
          end

          if !options[:titles_only] && searchable_options[:search_journals]
            r |= fetch_ranks_and_ids(
              search_scope(user, projects, options).
                joins(:journals).
                where("#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes)})", false).
                where(search_tokens_condition(["#{Journal.table_name}.notes"], tokens, options[:all_words])),
              options[:limit]
            )
            queries += 1
          end
        end

        if searchable_options[:search_attachments] && (options[:titles_only] ? options[:attachments] == 'only' : options[:attachments] != '0')
          r |= fetch_ranks_and_ids(
            search_scope(user, projects, options).
              joins(:attachments).
              where(search_tokens_condition(["#{Attachment.table_name}.filename", "#{Attachment.table_name}.description"], tokens, options[:all_words])),
            options[:limit]
          )
          queries += 1
        end

        if queries > 1
          r = r.sort.reverse
          if options[:limit] && r.size > options[:limit]
            r = r[0, options[:limit]]
          end
        end

        r
      end

      def search_token_match_statement_with_easy_extensions(column, value='?')
        column = "CAST(#{column} AS TEXT)" if column == "#{self.table_name}.id" && Redmine::Database.postgresql?
        search_token_match_statement_without_easy_extensions(column, value)
      end

      def self_and_descendants_with_easy_extensions(issues = nil)
        scope = Issue.joins("JOIN #{Issue.table_name} ancestors" +
                      " ON ancestors.root_id = #{Issue.table_name}.root_id" +
                      " AND ancestors.lft <= #{Issue.table_name}.lft AND ancestors.rgt >= #{Issue.table_name}.rgt"
        )
        scope = scope.where(:ancestors => {:id => issues.map(&:id)}) if issues.is_a?(Array)
        scope
      end

      def cross_project_scope_with_easy_extensions(project, scope=nil)
        issues_scope = cross_project_scope_without_easy_extensions(project, scope)

        if project && project.easy_is_easy_template?
          issues_scope.templates
        else
          issues_scope.non_templates
        end
      end

      def visible_condition_with_easy_extensions(user, options={})
        Project.allowed_to_condition(user, :view_issues, options) do |role, user|
          sql = if user.id && user.logged?
            case role.issues_visibility
            when 'all'
              '1=1'
            when 'default'
              user_ids = [user.id] + user.groups.map(&:id).compact
              "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
            when 'own'
              user_ids = [user.id] + user.groups.map(&:id).compact
              "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}) OR EXISTS (SELECT w.id FROM #{Watcher.table_name} w WHERE w.watchable_type = 'Issue' AND w.watchable_id = #{Issue.table_name}.id AND w.user_id = #{user.id}))"
            else
              '1=0'
            end
          else
            "(#{table_name}.is_private = #{connection.quoted_false})"
          end
          unless role.permissions_all_trackers?(:view_issues)
            tracker_ids = role.permissions_tracker_ids(:view_issues)
            if tracker_ids.any?
              sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
            else
              sql = '1=0'
            end
          end
          sql
        end
      end

      def easy_merge_and_close_issues(issues, merge_to)
        return false if issues.count == 1 && issues.first == merge_to

        issues = issues - [merge_to]
        merged = true
        close_status = IssueStatus.where(:is_closed => true).first
        updated_description = merge_to.description.to_s.dup

        issues.each do |issue|
          merged = false if !issue.easy_merge_to(merge_to, close_status)
          Mailer.with_deliveries(false) do
            issue.init_journal(User.current, I18n.t(:label_merged_into, id: "##{merge_to.id}")).save
          end
          updated_description << easy_merge_entity_description(issue)
        end

        begin
          merge_to.description = updated_description
          Mailer.with_deliveries(false) do
            merge_to.save
          end
        rescue ActiveRecord::StaleObjectError
          # if it is parent, it is changed during merging
          merge_to.reload
          merge_to.description = updated_description
          merge_to.save
        end

        issues_in_note = issues.collect { |issue| "##{issue.id}" }.join(', ')
        merge_to.init_journal(User.current, I18n.t(:label_merged_from, :ids => "#{issues_in_note}")).save

        merged
      end

      def easy_merge_entity_description(merging_issue)
        "\r\n" << '-' * 60 << ' ' << I18n.t(:label_merged_from, :ids => "##{merging_issue.id}") << "\r\n" << merging_issue.description.to_s
      end

      def count_and_group_by_with_easy_extensions(options)
        assoc = reflect_on_association(options[:association])
        select_field = assoc.foreign_key

        Issue.
          visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
          joins(:status, assoc.name).
          group(:status_id, :is_closed, "#{Issue.table_name}.#{select_field}").
          count.
          map do |columns, total|
          status_id, is_closed, field_value = columns
          is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
          {
            'status_id' => status_id.to_s,
            'closed' => is_closed,
            select_field => field_value.to_s,
            'total' => total.to_s
          }
        end
      end

    end

    module InstanceMethods

      def status_with_easy_extensions=(status)
        # reassign custom field values to ensure compliance with workflow
        reassign_custom_field_values if status != self.status
        send :status_without_easy_extensions=, status
      end

      def available_custom_fields_with_easy_extensions
        self.class.available_custom_fields_from_cache(project_id, tracker_id) ||
          ( (project && tracker) ? (project.all_issue_custom_fields.with_group & tracker.custom_fields.with_group) : [] )
      end

      def cache_key_with_easy_extensions
        if new_record?
          'issues/new'
        else
          "issues/#{id}-#{updated_on.strftime('%Y%m%d%H%M%S')}"
        end
      end

      def after_create_from_copy_with_easy_extensions
        return unless copy? && !@after_create_from_copy_handled

        if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
          if @current_journal
            @copied_from.init_journal(@current_journal.user)
          end
          relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
          unless relation.save
            logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
          end
        end

        @copied_issue_ids ||= {@copied_from.id => self.id}
        if !@copied_from.leaf? && @copy_options[:subtasks] != false
          copy_options = (@copy_options || {}).merge(:subtasks => false)
          attrs = self.attributes_for_descendants
          descendants = @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").to_a
          descendants.each do |child|
            # Do not copy self when copying an issue as a descendant of the copied issue
            next if child == self
            # Do not copy subtasks of issues that were not copied
            next unless @copied_issue_ids[child.parent_id]
            # Do not copy subtasks that are not visible to avoid potential disclosure of private data
            unless child.visible?
              logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
              next
            end
            if @copied_from.easy_is_repeating
              attributes_to_copy = (child.easy_repeat_settings['entity_attributes'] || {}).merge({:easy_is_repeating => false, :easy_repeat_settings => nil})
              copy = child.copy(attributes_to_copy, copy_options)
            else
              copy = Issue.new.copy_from(child, copy_options)
            end
            if @current_journal
              copy.init_journal(@current_journal.user)
            end
            copy.safe_attributes = attrs.dup if attrs

            custom_field_values = child.custom_field_values.inject({}) { |h, v| h[v.custom_field_id] = v.value; h }
            if attrs
              custom_field_values = custom_field_values.merge(attrs['custom_field_values'] || {})
            end
            copy.custom_field_values = custom_field_values

            copy.mass_operations_in_progress = true
            copy.author = author
            copy.project = project
            copy.parent_issue_id = @copied_issue_ids[child.parent_id]
            unless copy.save
              logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
              next
            end
            @copied_issue_ids[child.id] = copy.id
          end
        end
        @after_create_from_copy_handled = true
      end

      def assignable_users_with_easy_extensions
        return @assignable_users unless @assignable_users.nil?
        user_ids = []
        user_ids << author_id if author && author.active?
        user_ids << assigned_to_id if assigned_to && assigned_to.is_a?(User)

        if project && !User.current.limit_assignable_users_for_project?(project)
          project_scope = project.assignable_users(tracker)
          if user_ids.empty?
            @assignable_users = project_scope.to_a
            return @assignable_users
          end
          user_ids.concat project_scope.reorder(nil).pluck(:id)
        end

        @assignable_users = user_ids.empty? ? [] : Principal.where(:id => user_ids).sorted.to_a
      end

      def reload_with_easy_extensions(*args)
        @assignable_users = nil
        reload_without_easy_extensions(*args)
      end

      def journalized_attribute_names_with_easy_extensions
        attrs = journalized_attribute_names_without_easy_extensions - self.class.journalized_options[:non_journalized_columns]
        attrs -= ['estimated_hours'] unless User.current.allowed_to?(:view_estimated_hours, self.project)
        attrs
      end

      def editable_with_easy_extensions?(user = User.current)
        @editable = project && project.active? && editable_without_easy_extensions?(user) if @editable.nil?
        @editable
      end

      def attributes_editable_with_easy_extensions?(user = User.current)
        return false unless project && project.active?
        if @attributes_editable.nil?
          @attributes_editable = attributes_editable_without_easy_extensions?(user) ||
            ((self.author_id == user.id) && user_tracker_permission?(user, :edit_own_issue)) ||
            ((self.assigned_to_id == user.id) && user_tracker_permission?(user, :edit_assigned_issue))
        end
        @attributes_editable
      end

      def safe_attributes_with_easy_extensions=(attrs, user = User.current)
        @should_send_invitation_update = !!attrs.delete(:should_send_invitation_update) if attrs.is_a?(Hash)

        return unless attrs.is_a?(Hash)

        if attrs
          if !attrs['fixed_version_id'].blank? && (current_version = Version.find_by_id(attrs['fixed_version_id'])) # the version is changing

            if !attrs['old_fixed_version_id'].blank?
              previous_version = Version.find_by_id(attrs['old_fixed_version_id'])
            elsif !self.fixed_version_id_was.blank?
              previous_version = Version.find_by_id(self.fixed_version_id_was) if self.fixed_version_id_was
            end

            if attrs.key?('due_date')
              attrs_due_date = begin
                attrs['due_date'].to_date;
              rescue;
                nil;
              end
            else
              attrs_due_date = self.due_date
            end

            if previous_version && ((attrs_due_date.blank? && (previous_version != current_version)) || (attrs_due_date == previous_version.due_date))
              attrs_due_date = current_version.due_date
            end

            if previous_version.nil? && attrs_due_date.blank?
              attrs_due_date = current_version.due_date
            end

            attrs['due_date'] = attrs_due_date
          end
          attrs.delete('old_fixed_version_id')

          if self.current_journal && attrs[:without_notifications] && User.current.allowed_to?(:edit_without_notifications, self.project)
            self.current_journal.notify = false
          end
        end

        # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed

        send :safe_attributes_without_easy_extensions=, attrs, user
        Redmine::Hook.call_hook(:after_safe_attributes_assigned_issue, :attrs => attrs, :user => user, :issue => self)

        @read_only_attribute_names[(user || User.current).id] = nil if @read_only_attribute_names
        @required_attribute_names[(user || User.current).id] = nil if @required_attribute_names
      end

      def deletable_with_easy_extensions?(user=User.current)
        return false unless project && project.active?
        deletable_without_easy_extensions?(user)
      end

      def after_project_change_with_easy_extensions
        after_project_change_without_easy_extensions
      rescue ActiveRecord::Rollback
        errors.add :base, l(:error_invalid_subtasks)
        raise
      end

      def validate_issue_with_easy_extensions
        if self.due_date && self.start_date && (start_date_changed? || due_date_changed?) && self.due_date < self.start_date
          errors.add :base, l(:greater_than_start_date_human_error, :distance => distance_of_time_in_words(self.start_date, self.due_date), :due_date => "#{self.due_date.day}. #{self.due_date.month}.")
        end

        if self.start_date && start_date_changed? && self.soonest_start && self.start_date < self.soonest_start
          errors.add :base, l(:greater_than_soonest_start_human_error, :distance => distance_of_time_in_words(self.start_date, self.soonest_start), :sonnest_start => "#{self.soonest_start.day}. #{self.soonest_start.month}.")
        end

        if project
          if fixed_version
            if !assignable_versions.include?(fixed_version)
              errors.add :fixed_version_id, :inclusion
            elsif reopening? && fixed_version.closed?
              errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
            end
          end

          # Checks that the issue can not be added/moved to a disabled tracker
          if project && (tracker_id_changed? || project_id_changed?)
            if tracker && !project.trackers.include?(tracker)
              errors.add :base, l(:error_no_tracker_in_project)
            end
          end
        end

        # Checks parent issue assignment
        if @invalid_parent_issue_id.present?
          errors.add :parent_issue_id, :invalid
        elsif @parent_issue
          if !valid_parent_project?(@parent_issue)
            errors.add :parent_issue_id, :invalid
          elsif (@parent_issue != parent) && (
              self.would_reschedule?(@parent_issue) ||
                @parent_issue.self_and_ancestors.any? {|a| a.relations_from.any? {|r| r.relation_type == IssueRelation::TYPE_PRECEDES && r.issue_to.would_reschedule?(self)}}
            )
            errors.add :parent_issue_id, :invalid
          elsif !new_record?
            # moving an existing issue
            if move_possible?(@parent_issue)
              # move accepted
            else
              errors.add :parent_issue_id, :invalid
            end
          end
        end

        if self.fixed_version && self.fixed_version.effective_date && self.due_date
          if self.fixed_version.effective_date < self.due_date && !EasySetting.value('milestone_effective_date_from_issue_due_date')
            ef_date = self.fixed_version.effective_date
            errors.add :base, l(:before_milestone_human_error, :distance => distance_of_time_in_words(self.due_date, ef_date), :effective_date => "#{ef_date.day}. #{ef_date.month}.")
          end
        end

        if !EasySetting.value('project_calculate_due_date') && self.project && !self.project.due_date.blank?
          if self.due_date && self.due_date > self.project.due_date
            errors.add :base, l(:before_project_end_human_error, :distance => distance_of_time_in_words(self.due_date, self.project.due_date), :project_due_date => "#{self.project.due_date.day}. #{self.project.due_date.month}.")
          end
        end

        if !EasySetting.value('project_calculate_start_date') && self.project && !self.project.start_date.blank?
          if self.start_date && self.start_date < self.project.start_date
            errors.add :base, l(:after_project_start_human_error, :distance => distance_of_time_in_words(self.start_date, self.project.start_date), :project_start_date => "#{self.project.start_date.day}. #{self.project.start_date.month}.")
          end
        end
      end

      def visible_with_easy_extensions?(usr=nil)
        (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
          visible = if user.logged?
            case role.issues_visibility
            when 'all'
              true
            when 'default'
              !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
            when 'own'
              self.author == user || user.is_or_belongs_to?(assigned_to) || self.watcher_user_ids.include?(user.id)
            else
              false
            end
          else
            !self.is_private?
          end
          unless role.permissions_all_trackers?(:view_issues)
            visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
          end
          visible
        end
      end

      def css_classes_with_easy_extensions(user=User.current, lvl=nil, options={})
        inline_editable = options[:inline_editable] != false
        css = css_classes_without_easy_extensions(user)
        if lvl && lvl > 0
          css << ' idnt'
          css << " idnt-#{lvl}"
        end
        css << ' multieditable-container' if inline_editable

        scheme = case EasySetting.value('issue_color_scheme_for')
        when 'issue_priority'
          priority.try(:easy_color_scheme)
        when 'issue_status'
          status.try(:easy_color_scheme)
        when 'tracker'
          tracker.try(:easy_color_scheme)
        end
        css << " scheme #{scheme}" if scheme.present?

        return css
      end

      def estimated_hours_with_easy_extensions=(h)
        write_attribute :estimated_hours, (h.is_a?(String) ? (h.to_hours || h) : h)
      end

      def to_s_with_easy_extensions
        if EasySetting.value('show_issue_id', self.project_id)
          to_s_with_id
        else
          to_s_without_id
        end
      end

      def recalculate_attributes_for_with_easy_extensions(issue_id)
        if issue_id && p = Issue.find_by_id(issue_id)
          text = "#{l(:label_issue_automatic_recalculate_attributes, :issue_id => "##{self.id}")}"
          if Setting.text_formatting == 'HTML'
            text = "<p>#{text}</p>"
          end
          journal = p.init_journal(User.current, text)
          something_changed = false

          if p.priority_derived?
            # priority = highest priority of open children
            # priority is left unchanged if all children are closed and there's no default priority defined
            if priority_position = p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
              parent_new_priority = IssuePriority.find_by_position(priority_position)
            elsif default_priority = IssuePriority.default
              parent_new_priority = default_priority
            end
            if p.priority != parent_new_priority
              p.priority = parent_new_priority
              something_changed = true
            end
          end

          if p.dates_derived?
            # start/due dates = lowest/highest dates of children
            parent_new_start_date = p.children.minimum(:start_date)
            parent_new_due_date = p.children.maximum(:due_date)

            if parent_new_start_date && parent_new_due_date && parent_new_due_date < parent_new_start_date
              parent_new_start_date, parent_new_due_date = parent_new_due_date, parent_new_start_date
            end

            if parent_new_start_date != p.start_date || parent_new_due_date != p.due_date
              p.start_date = parent_new_start_date
              p.due_date = parent_new_due_date
              something_changed = true
            end
          end

          if p.done_ratio_derived?
            # done ratio = weighted average ratio of leaves
            unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
              child_count = p.children.count
              if child_count > 0
                average = p.children.where('estimated_hours > 0').average(:estimated_hours).to_f
                if average == 0
                  average = 1
                end
                done = p.children.joins(:status).
                  sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
                        "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
                progress = done / (average * child_count)
                parent_new_done_ratio = progress.round

                if parent_new_done_ratio != p.done_ratio
                  p.done_ratio = parent_new_done_ratio
                  something_changed = true
                end
              end
            end
          end

          if something_changed
            # ancestors will be recursively updated
            Redmine::Hook.call_hook(:model_issue_before_automatic_change_from_subtask, {:issue => p, :journal => journal})
            p.mass_operations_in_progress = true
            self.mass_operations_in_progress = true
            p.save(:validate => false)
          end
        end
      end

      def reschedule_on_with_easy_extensions!(date)
        return if date.nil? || self.mass_operations_in_progress

        self.init_journal(User.current)

        reschedule_on_without_easy_extensions!(date)
      end

      def overdue_with_easy_extensions?
        if due_date.nil?
          false
        elsif due_date.is_a?(Date)
          overdue_without_easy_extensions?
        else
          (due_date < Time.now) && !closed?
        end
      end

      # Returns an array of statuses that user is able to apply
      def new_statuses_allowed_to_with_easy_extensions(user=User.current, include_default=false)
        if new_record? && @copied_from
          [default_status, @copied_from.status].compact.uniq.sort
        else
          if new_record?
            # nop
          elsif tracker_id_changed?
            if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
              initial_status = default_status
            elsif tracker && tracker.issue_status_ids.include?(status_id_was)
              initial_status = IssueStatus.find_by_id(status_id_was)
            else
              initial_status = default_status
            end
          else
            initial_status = status_was
          end

          initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
          assignee_transitions_allowed = initial_assigned_to_id.present? &&
            (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))

          if user.admin? && EasySetting.value('skip_workflow_for_admin', project)
            statuses = IssueStatus.sorted.to_a
          else
            if project
              if user.admin?
                user_roles = project.user_roles(user)
                user_roles = project.all_members_roles if user_roles.empty?
              else
                user_roles = user.roles_for_project(project)
              end
            else
              user_roles = []
            end

            statuses = []
            statuses += IssueStatus.new_statuses_allowed(
              initial_status,
              user_roles,
              tracker,
              author == user,
              assignee_transitions_allowed
            )
            statuses << initial_status unless statuses.empty?
            statuses << default_status if include_default || (new_record? && statuses.empty?)
            statuses = statuses.compact.uniq.sort
          end

          if blocked?
            statuses.reject!(&:is_closed?)
          end
          statuses
        end
      end

      def notified_users_with_easy_extensions
        n_users = notified_users_without_easy_extensions

        # notify previous assignee
        jd = self.journals.preload(:details).order(created_on: :desc).collect { |j| j.details.detect { |d| d.prop_key == 'assigned_to_id' } }.compact
        previous_assignee = User.active.where(:id => jd[0].old_value).first if jd.size > 0 && !jd[0].old_value.blank?

        n_users << previous_assignee if previous_assignee && previous_assignee.active? && !n_users.include?(previous_assignee) && (previous_assignee.mail_notification == 'all' || previous_assignee.mail_notification == 'only_my_events')

        # if issue is closed notify second previous assignee
        if self.status && self.status.is_closed?
          second_previous_assignee = User.active.where(:id => jd[1].old_value).first if jd.size > 1 && !jd[1].old_value.blank?
          n_users << second_previous_assignee if second_previous_assignee && second_previous_assignee.active? && !n_users.include?(second_previous_assignee) && (second_previous_assignee.mail_notification == 'all' || second_previous_assignee.mail_notification == 'only_my_events')
        end

        if self.closed? && !self.closing?
          n_users.reject! { |u| u.pref.no_notified_if_issue_closing }
        end

        n_users
      end

      def relations_with_easy_extensions
        @relations ||= IssueRelation::Relations.new(self, (relations_from.preload(:issue_to => [:project, :assigned_to, :tracker, :priority, :status]).to_a + relations_to.preload(:issue_from => [:project, :assigned_to, :tracker, :priority, :status]).to_a).sort)
      end

      # Adds a cache even if user is User.current wich should be same as user.nil?
      # TODO: too many if statements. user_roles != roles_for_project? than delete project
      def workflow_rule_by_attribute_with_easy_extensions(user=nil)
        user_real = user || User.current
        user = nil if user_real.id == User.current.id
        return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?

        if project
          if user_real.admin? && EasySetting.value('skip_workflow_for_admin', project)
            roles = ( RequestStore.store['all_roles_for_admin'] ||= Role.all.to_a )
          elsif user_real.admin?
            roles = user_real.roles_for_project(project)
            roles = project.all_members_roles.to_a if roles.empty?
          else
            roles = user_real.roles_for_project(project)
          end
        else
          roles = []
        end
        roles = roles.select(&:consider_workflow?)
        return {} if roles.empty?

        result = {}

        workflow_rules = WorkflowPermission.easy_rules_by_role_id(status_id, tracker_id, roles.map(&:id))
        if workflow_rules.present?
          roles.each do |role|
            # fields with roles are in method - there are cached - performance for editable on index
            fields_with_roles = Issue.non_visible_custom_field_with_roles
            fields_with_roles.each do |field_id, role_ids|
              unless role_ids.include?(role.id)
                field_name = field_id.to_s
                workflow_rules[field_name] ||= {}
                workflow_rules[field_name][role.id] = 'readonly'
              end
            end
          end
          roles_size = roles.size
          workflow_rules.each do |attr, rules|
            next if rules.size < roles_size
            uniq_rules = rules.values.uniq
            if uniq_rules.size == 1
              result[attr] = uniq_rules.first
            else
              result[attr] = 'required'
            end
          end
        end
        @workflow_rule_by_attribute = result
        result
      end

      def read_only_attribute_names_with_easy_extensions(user=nil)
        @read_only_attribute_names ||= {}
        @read_only_attribute_names[(user || User.current).id] ||= read_only_attribute_names_without_easy_extensions(user)
      end

      def required_attribute_names_with_easy_extensions(user=nil)
        @required_attribute_names ||= {}
        @required_attribute_names[(user || User.current).id] ||= required_attribute_names_without_easy_extensions(user)
      end

      def send_notification_with_easy_extensions
        return if self.author && self.author.pref.no_notification_ever

        if notify? && Setting.notified_events.include?('issue_added')
          Mailer.send_mail_issue_add(self)
          self.notification_sent = true
        end
      end

      def copy_from_with_easy_extensions(arg, options={})
        copy = copy_from_without_easy_extensions(arg, options)

        copy.author = @copied_from.author if options[:copy_author]
        copy.attributes.except!('easy_closed_by_id')

        Redmine::Hook.call_hook(:model_issue_copy_from, {hook_caller: self, copy: copy, copied_from: @copied_from})

        copy
      end

      def get_issue_history
        replace_history_token('%task_history%')
      end

      def replace_history_token(text)
        text.gsub!(/%\s?task_history\s?%/) do |token|
          journals_with_notes = journals.visible.where(:private_notes => false).with_notes.order(:created_on => :desc)
          history = ''

          if journals_with_notes.exists?
            history = '<div>'
            history << content_tag(:h4, I18n.t(:label_history))

            journals_with_notes.each do |journal|
              history << format_journal_for_mail_template(journal)
            end
            history << '</div>'
          end

          history
        end
        text
      end

      def replace_last_non_private_comment(text, last_journal = nil)
        last_non_private_journal = nil
        text.gsub!(/%\s?task_last_journal\s?%/) do |token|
          if last_non_private_journal.nil?
            last_journal_scope = journals.visible.where(:private_notes => false).
              with_notes.order(:created_on => :desc)
            last_journal_scope = last_journal_scope.where.not(:id => last_journal.id) unless last_journal.nil?
            last_non_private_journal = last_journal_scope.first
          end

          format_journal_for_mail_template(last_non_private_journal)
        end
        text
      end

      def format_journal_for_mail_template(journal)
        return '' if journal.nil?

        authoring = content_tag(:strong, l(:label_updated_datetime_by, :author => journal.user, :datetime => format_time(journal.created_on)))
        parsed_notes = journal.notes.html_safe
        parsed_notes = content_tag(:p, parsed_notes) unless parsed_notes =~ /^\s?<p>.*<\/p>\s?$/

        authoring << parsed_notes
      end

    end

  end
end
EasyExtensions::PatchManager.register_model_patch 'Issue', 'EasyPatch::IssuePatch'
