# redmics - redmine ics export plugin
# Copyright (c) 2011-2022  Frank Schwarz, frank.schwarz@buschmais.com
#
# 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.

module Redmics
  class Export
    include Redmics
    include Redmine::I18n

    def initialize(controller)
      @controller = controller
      @priority_count = IssuePriority.all.length
    end

    def settings(args)
      @user = args[:user]
      @project = args[:project]
      @query = args[:query]
      @status = args[:status]
      @alarm = args[:alarm]
      @assignment = args[:assignment]
      @issue_strategy = args[:issue_strategy]
      @version_strategy = args[:version_strategy]
      @summary_strategy = args[:summary_strategy]
      @description_strategy = args[:description_strategy]
    end

    def icalendar
      issues_renderer = create_issues_renderer @issue_strategy
      versions_renderer = create_versions_renderer @version_strategy

      if @query
        (issues, versions) = redmine_query
      else
        (issues, versions) = redmics_query
      end

      events = []
      events += issues.collect(&issues_renderer).to_a.flatten
      events += versions.collect(&versions_renderer).to_a.flatten

      cal = Icalendar::Calendar.new
      cal.publish
      events.each { |event| cal.add_event(event) }
      return cal
    end

  private

    def redmine_query
      begin
        issues = []
        versions = []
        if @query.valid?
          # query: issues
          issues = @query.issues(
            :include => [:tracker, :assigned_to, :priority, :fixed_version, :author],
          ) unless @issue_strategy == :none
          # query: versions -> skip
        end
      rescue Exception => e
        # we will just deliver an empty ical file instead of showing an error page
        @controller.logger.warn('No issues have been selected. ' + e.to_s)
      end
      return [issues, versions]
    end

    def redmics_query
      begin
        case @status
        when :open
          issue_status_condition = ["#{IssueStatus.table_name}.is_closed = ?", false]
          version_status_condition = ["#{Version.table_name}.status <> ?", 'closed']
        when :all
          issue_status_condition = []
          version_status_condition = []
        else
          raise "Unknown status: '#{@status}'."
        end

        case @assignment
        when :my
          raise 'Anonymous user cannot have issues assigned.' if @user.anonymous?
          assigned_to_condition = ["assigned_to_id = #{@user.id}"]
        when :assigned
          assigned_to_condition = ["assigned_to_id is not null"]
        when :all
          assigned_to_condition = []
        else
          raise "Unknown assignment: '#{@assignment}.'"
        end

        @query = IssueQuery.new(:name => "_")
        @query.project = @project
        @query.filters = nil
        issues = []
        versions = []

        # query: issues
        c = QueryConditions.new()
        c << issue_status_condition unless issue_status_condition.empty?
        c << assigned_to_condition unless assigned_to_condition.empty?
        issues = @query.issues(
          :include => [:tracker, :assigned_to, :priority, :fixed_version, :author],
          :conditions => c.conditions) unless @issue_strategy == :none
        # query: versions
        c = QueryConditions.new()
        c << version_status_condition unless version_status_condition.empty?
        versions =  @query.versions(
          :conditions => c.conditions
        ) unless @version_strategy == :none
        c << ["#{Version.table_name}.sharing = ?", 'system']
        versions << @query.versions(
          :conditions => c.conditions
        ) unless @version_strategy == :none
        versions.flatten!
      rescue Exception => e
        # we will just deliver an empty ical file instead of showing an error page
        @controller.logger.warn('No issues have been selected. ' + e.to_s)
        issues = []
        versions = []
      end
      return [issues, versions]
    end

    def create_issues_renderer(type)
      case type
      when :none
        lambda { |issue|
          []
        }
      when :vevent_full_span
        lambda { |issue|
          result = create_issue_vevent_full_span(issue)
          apply_issue_common_properties(issue, result)
          apply_issue_event_properties(issue, result)
          apply_issue_alarm(issue, result) unless @alarm.nil?
          enhance_issue_summary(issue, result)
          enhance_issue_description(issue, result)
          result
        }
      when :vevent_end_date
        lambda { |issue|
          result = create_issue_vevent_end_date(issue)
          apply_issue_common_properties(issue, result)
          apply_issue_event_properties(issue, result)
          apply_issue_alarm(issue, result) unless @alarm.nil?
          enhance_issue_summary(issue, result)
          enhance_issue_description(issue, result)
          result
        }
      when :vevent_start_and_end_date
        lambda { |issue|
          result = create_issue_vevent_start_and_end_date(issue)
          apply_issue_common_properties(issue, result)
          apply_issue_event_properties(issue, result)
          apply_issue_alarm(issue, result) unless @alarm.nil?
          enhance_issue_summary(issue, result)
          enhance_issue_description(issue, result)
          result
        }
      when :vtodo
        lambda { |issue|
          result = create_issue_vtodo(issue)
          apply_issue_common_properties(issue, result)
          apply_issue_todo_properties(issue, result)
          apply_issue_alarm(issue, result) unless @alarm.nil?
          enhance_issue_summary(issue, result)
          enhance_issue_description(issue, result)
          result
        }
      end
    end

    def create_versions_renderer(type)
      case type
      when :none
        lambda { |version|
          []
        }
      when :vevent_full_span
        lambda { |version|
          result = create_version_vevent_full_span(version)
          apply_version_common_properties(version, result)
          apply_version_event_properties(version, result)
          enhance_version_description(version, result)
          result
        }
      when :vevent_end_date
        lambda { |version|
          result = create_version_vevent_end_date(version)
          apply_version_common_properties(version, result)
          apply_version_event_properties(version, result)
          enhance_version_description(version, result)
          result
        }
      when :vevent_start_and_end_date
        lambda { |version|
          result = create_version_vevent_start_and_end_date(version)
          apply_version_common_properties(version, result)
          apply_version_event_properties(version, result)
          enhance_version_description(version, result)
          result
        }
      when :vtodo
        lambda { |version|
          result = create_version_vtodo(version)
          apply_version_common_properties(version, result)
          apply_version_todo_properties(version, result)
          enhance_version_description(version, result)
          result
        }
      end
    end


    def create_issue_vevent_full_span(issue)
      start_date, due_date = issue_period(issue)
      return [] if start_date.nil? || due_date.nil?
      event = Icalendar::Event.new
      event.dtstart = Icalendar::Values::Date.new(start_date)
      event.dtend = Icalendar::Values::Date.new(due_date + 1)
      event.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}@#{Setting.host_name}"
      return [event]
    end

    def create_issue_vevent_end_date(issue)
      due_date = issue_period(issue)[1]
      return [] if due_date.nil?
      event = Icalendar::Event.new
      event.dtstart = Icalendar::Values::Date.new(due_date)
      event.dtend = Icalendar::Values::Date.new(due_date + 1)
      event.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}@#{Setting.host_name}"
      return [event]
    end

    def create_issue_vevent_start_and_end_date(issue)
      start_date, due_date = issue_period(issue)
      if start_date.nil? && due_date.nil?
        return []
      elsif start_date == due_date
        event = Icalendar::Event.new
        event.dtstart = Icalendar::Values::Date.new(start_date)
        event.dtend = Icalendar::Values::Date.new(start_date + 1)
        event.summary = "<> #{issue.subject}"
        event.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}@#{Setting.host_name}"
        return [event]
      end
      result = []
      unless start_date.nil?
        event = Icalendar::Event.new
        event.dtstart = Icalendar::Values::Date.new(start_date)
        event.dtend = Icalendar::Values::Date.new(start_date + 1)
        event.summary = "> #{issue.subject}"
        event.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}:s@#{Setting.host_name}"
        result << event
      end
      unless due_date.nil?
        event = Icalendar::Event.new
        event.dtstart = Icalendar::Values::Date.new(due_date)
        event.dtend = Icalendar::Values::Date.new(due_date + 1)
        event.summary = "< #{issue.subject}"
        event.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}:e@#{Setting.host_name}"
        result << event
      end
      return result
    end

    def create_issue_vtodo(issue)
      start_date, due_date = issue_period(issue)
      todo = Icalendar::Todo.new
      unless start_date.nil?
        todo.dtstart = Icalendar::Values::Date.new(start_date)
      end
      unless due_date.nil?
        todo.due = Icalendar::Values::Date.new(due_date)
      end
      todo.uid = "id:redmics:project:#{issue.project_id}:issue:#{issue.id}@#{Setting.host_name}"
      return [todo]
    end

    def apply_issue_common_properties(issue, result)
      result.each { |event|
        event.summary = "#{issue.subject}" unless event.summary
        event.priority = map_priority issue.priority.position
        event.created = Icalendar::Values::Date.new(issue.created_on)
        event.last_modified = issue.updated_on.to_datetime unless issue.updated_on.nil?
        event.description = issue.description unless issue.description.nil?
        event.categories = [@controller.l(:label_issue).upcase]
        event.contact = Icalendar::Values::Text.new(issue.assigned_to.name, 
          {"ALTREP" => "mailto:#{issue.assigned_to.mail}"}) unless issue.assigned_to.nil?
        event.organizer = Icalendar::Values::CalAddress.new("mailto:#{issue.author.mail}", cn: issue.author.name)
        event.url = @controller.url_for(:controller => 'issues', :action => 'show', :id => issue.id)
        event.sequence = issue.lock_version
      }
    end

    def apply_issue_alarm(issue, result)
      if !result.empty?
        alarm_trigger = @alarm # strange but seems to be required
        result.last.alarm { |alarm|
          alarm.description = "This is an event reminder"
          alarm.trigger = alarm_trigger
        }
      end
    end

    def apply_issue_event_properties(issue, result)
      result.each { |event|
        event.status = issue.assigned_to ? "CONFIRMED" : "TENTATIVE" unless issue.closed?
      }
    end

    def apply_issue_todo_properties(issue, result)
      result.each { |todo|
        if issue.closed?
          todo.status = "COMPLETED"
          todo.completed = issue.updated_on.to_datetime
          todo.percent_complete = 100
        elsif issue.assigned_to
          todo.status = "IN-PROCESS"
          todo.percent_complete = issue.done_ratio ? issue.done_ratio.to_i : 0
        else
          todo.status = "NEEDS-ACTION"
        end
      }
    end

    def create_version_vevent_full_span(version)
      start_date, due_date = version_period(version)
      return [] if start_date.nil? || due_date.nil?
      event = Icalendar::Event.new
      event.dtstart = Icalendar::Values::Date.new(start_date)
      event.dtend = Icalendar::Values::Date.new(due_date + 1)
      event.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}@#{Setting.host_name}"
      return [event]
    end

    def create_version_vevent_end_date(version)
      due_date = version_period(version)[1]
      return [] if due_date.nil?
      event = Icalendar::Event.new
      event.dtstart = Icalendar::Values::Date.new(due_date)
      event.dtend = Icalendar::Values::Date.new(due_date + 1)
      event.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}@#{Setting.host_name}"
      return [event]
    end

    def create_version_vevent_start_and_end_date(version)
      start_date, due_date = version_period(version)
      if start_date.nil? && due_date.nil?
        return []
      elsif start_date == due_date
        event = Icalendar::Event.new
        event.dtstart = Icalendar::Values::Date.new(start_date)
        event.dtend = Icalendar::Values::Date.new(start_date + 1)
        event.summary = "<#> #{l(:label_version)} #{version.name}"
        event.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}@#{Setting.host_name}"
        return [event]
      end
      result = []
      unless start_date.nil?
        event = Icalendar::Event.new
        event.dtstart = Icalendar::Values::Date.new(start_date)
        event.dtend = Icalendar::Values::Date.new(start_date + 1)
        event.summary = ">> #{l(:label_version)} #{version.name}"
        event.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}:s@#{Setting.host_name}"
        result << event
      end
      unless due_date.nil?
        event = Icalendar::Event.new
        event.dtstart = Icalendar::Values::Date.new(due_date)
        event.dtend = Icalendar::Values::Date.new(due_date + 1)
        event.summary = "<< #{l(:label_version)} #{version.name}"
        event.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}:e@#{Setting.host_name}"
        result << event
      end
      return result
    end

    def create_version_vtodo(version)
      start_date, due_date = version_period(version)
      todo = Icalendar::Todo.new
      unless start_date.nil?
        todo.dtstart = Icalendar::Values::Date.new(start_date)
      end
      unless due_date.nil?
        todo.due = Icalendar::Values::Date.new(due_date)
      end
      todo.uid = "id:redmics:project:#{version.project_id}:version:#{version.id}@#{Setting.host_name}"
      return [todo]
    end

    def apply_version_common_properties(version, result)
      result.each { |event|
        event.summary = "#{@controller.l(:label_version)} #{version.name}" unless event.summary
        event.created = Icalendar::Values::Date.new(version.created_on)
        event.last_modified = version.updated_on.to_datetime unless version.updated_on.nil?
        event.description = version.description unless version.description.nil?
        event.categories = [@controller.l(:label_version).upcase]
        event.url = @controller.url_for(:controller => 'versions', :action => 'show', :id => version.id)
        days = (version.updated_on.to_i - version.created_on.to_i) / 86400
        event.sequence = days
      }
    end

    def apply_version_event_properties(version, result)
      result.each { |event|
        event.status = "CONFIRMED" unless version.closed?
      }
    end

    def apply_version_todo_properties(version, result)
      result.each { |todo|
        if version.closed?
          todo.status = "COMPLETED"
          todo.completed = version.updated_on.to_datetime
          todo.percent_complete = 100
        else
          todo.status = "IN-PROCESS"
          todo.percent_complete = version.completed_percent.to_i
        end
      }
    end

    def enhance_issue_summary(issue, result)
      result.each { |item|
        case @summary_strategy
        when :plain
          # no action
        when :status
          item.summary = "#{item.summary} (#{issue.status.name})" if issue.status
        when :ticket_number_and_status
          item.summary = "#{item.summary} (#{issue.status.name})" if issue.status
          if /(<|>|<>) (.*)/ =~ item.summary
            m = Regexp.last_match
            item.summary = "#{m[1]} #{issue.tracker} ##{issue.id}: #{m[2]}"
          else
            item.summary = "#{issue.tracker} ##{issue.id}: #{item.summary}"
          end
        else
          raise "Unknown summary_strategy: '#{@summary_strategy}'."
        end
      }
    end

    def enhance_issue_description(issue, result)
      result.each { |item|
        case @description_strategy
        when :plain
          # no action
        when :url_and_version
          header = []
          header << "#{issue.tracker} ##{issue.id}: #{item.url}"
          header << "#{@controller.l(:field_project)}: #{issue.project.name}" if issue.project
          header << "#{@controller.l(:field_fixed_version)}: #{issue.fixed_version}" if issue.fixed_version
          if item.description
            item.description = header.join("\n") + "\n\n" + item.description
          else
            item.description = header.join("\n")
          end
        when :full_no_url
          header = []
          header << "#{issue.tracker} ##{issue.id}"
          header << "#{@controller.l(:field_project)}: #{issue.project.name}" if issue.project
          header << "#{@controller.l(:field_author)}: #{issue.author.name}" if issue.author
          header << "#{@controller.l(:field_status)}: #{issue.status.name}" if issue.status
          header << "#{@controller.l(:field_priority)}: #{issue.priority}" if issue.priority
          header << "#{@controller.l(:field_assigned_to)}: #{issue.assigned_to.name}" if issue.assigned_to
          header << "#{@controller.l(:field_category)}: #{issue.category.name}" if issue.category
          header << "#{@controller.l(:field_fixed_version)}: #{issue.fixed_version}" if issue.fixed_version
          if item.description
            item.description = header.join("\n") + "\n\n" + item.description
          else
            item.description = header.join("\n")
          end
        when :full
          header = []
          header << "#{issue.tracker} ##{issue.id}: #{item.url}"
          header << "#{@controller.l(:field_project)}: #{issue.project.name}" if issue.project
          header << "#{@controller.l(:field_author)}: #{issue.author.name}" if issue.author
          header << "#{@controller.l(:field_status)}: #{issue.status.name}" if issue.status
          header << "#{@controller.l(:field_priority)}: #{issue.priority}" if issue.priority
          header << "#{@controller.l(:field_assigned_to)}: #{issue.assigned_to.name}" if issue.assigned_to
          header << "#{@controller.l(:field_category)}: #{issue.category.name}" if issue.category
          header << "#{@controller.l(:field_fixed_version)}: #{issue.fixed_version}" if issue.fixed_version
          if item.description
            item.description = header.join("\n") + "\n\n" + item.description
          else
            item.description = header.join("\n")
          end
        else
          raise "Unknown description_strategy: '#{@description_strategy}'."
        end
      }
    end

    def enhance_version_description(version, result)
      result.each { |item|
        case @description_strategy
        when :plain
          # no action
        when :url_and_version
          header = []
          header << "#{@controller.l(:field_url)}: #{item.url}"
          if item.description
            item.description = header.join("\n") + "\n\n" + item.description
          else
            item.description = header.join("\n")
          end
        when :full_no_url
          header = []
          header << "#{@controller.l(:field_url)}"
          header << "#{@controller.l(:field_project)}: #{version.project.name}" if version.project
          header << "#{@controller.l(:field_status)}: #{version.status}" if version.status
          if item.description
            item.description = header.join("\n") + "\n\n" + item.description
          else
            item.description = header.join("\n")
          end
        when :full
          header = []
          header << "#{@controller.l(:field_url)}: #{item.url}"
          header << "#{@controller.l(:field_project)}: #{version.project.name}" if version.project
          header << "#{@controller.l(:field_status)}: #{version.status}" if version.status
          if item.description
            item.description = header.join("\n") + "\n\n" + item.description
          else
            item.description = header.join("\n")
          end
        else
          raise "Unknown description_strategy: '#{@description_strategy}'."
        end
      }
    end

    def issue_period(issue)
      start_date = issue.start_date || (issue.fixed_version.start_date unless issue.fixed_version.nil?)
      due_date = issue.due_date || (issue.fixed_version.due_date unless issue.fixed_version.nil?)
      return [start_date, due_date]
    end

    def version_period(version)
      return [version.start_date, version.due_date]
    end

    # isses_priority goes from 'low' (1), 'normal' (2) to 'immediate' (@priority_count)
    # icalendar priority goes from 'urgent' (1) to 'low' (9) (btw. 0 = undefined)
    def map_priority(isses_priority)
      case isses_priority
      when 1; 9
      when 2; 5
      when 3..@priority_count; 1
      else 9
      end
    end
  end
end
