# Trac syncing ditz plugin
#
# Provides ditz <-> trac synchronization
#
# Command added:
#   ditz sync: synchronize issues with Trac
#
# Usage:
#   1. add a line "- trac-sync" to the .ditz-plugins file in the project root.

require 'rubygems'
require 'trac4r'
require 'ditz'

module Ditz
  class Issue
    def log_at time, what, who, comment
      add_log_event([time, who, what, comment || ""])
      self
    end
  end

  class TracUtil
    # If new types or dispositions are added to either ditz or trac, they'll need
    # to be mapped here
    #
    # trac_type -> ditz_type
    TTYPE_DTYPE = { "defect" => :bugfix, 
      "enhancement" => :feature, 
      "task" => :task }
    DTYPE_TTYPE = TTYPE_DTYPE.invert
    # trac_resolution -> ditz_disposition
    RES_DISPO = { "fixed" => :fixed,
      "wontfix" => :wontfix,
      "worksforme" => :wontfix,
      "invalid" => :wontfix,
      "duplicate" => :duplicate,
      }
    DISPO_RES = RES_DISPO.invert
    # But, because it isn't 1-1, fix the mapping
    DISPO_RES[ :wontfix ] = "wontfix"
    DISPO_RES[ :reorg ]   = "wontfix"
    TSTATUS_DSTATUS = { "accepted" => :in_progress,
      "repoened" => :in_progress,
      "assigned" => :unstarted,
      "new" => :unstarted,
      "closed" => :closed,
      }
    DSTATUS_TSTATUS = TSTATUS_DSTATUS.invert
    # But, because it isn't 1-1, fix the mapping
    DSTATUS_TSTATUS[ :in_progress ] = "accepted" 
    DSTATUS_TSTATUS[ :unstarted ] = "assigned" 
    DSTATUS_TSTATUS[ :paused ] = "assigned" 

    def initialize( project, config, trac )
      @project, @config, @trac = project, config, trac
    end

    def equal?( ticket, issue )
      ticket.summary == issue.title
    end

    def pair( tickets )
      rv = []
      issues = @project.issues.clone
      for ticket in tickets 
        issue = issues.find { |i| equal?( ticket, i ) }
        issues.delete(issue) if issue
        rv << [ticket,issue]
      end
      for issue in issues
        rv << [nil,issue]
      end
      rv
    end

    def create_tickets( issues )
    end

    def create_issues( tickets )
      tickets.each do |t,i|   # i will always be nil
        # trac4r doesn't yet support resolution
        resolution = t.status == "closed" ? :fixed : nil
        release = @project.releases.find { |r| r.name == t.milestone }
        unless release 
          puts "Creating release #{t.milestone}"
          release = Ditz::Release.create({:name=>t.milestone}, [@config, @project])
          @project.add_release(release)
        end

        if release.status == :released
          puts "Orphaned ticket ##{t.id}: milestone #{t.milestone} already released!"
          puts "\t#{t.summary}"
        else
          issue = Ditz::Issue.create({:reporter => t.reporter,
                                     :creation_time => t.created_at.to_time,
                                     :title => t.summary,
                                     :type  => TTYPE_DTYPE[ t.type ],
                                     :desc  => t.description,
                                     :release => t.milestone,
                                     :component => t.component,
                                     :status => TSTATUS_DSTATUS[ t.status ],
                                     :disposition => resolution,
                                     :references => []
          }, [@config, @project])

          @project.add_issue( issue )
          puts "Created issue #{issue.id[0,4]}: #{issue.title}"
        end
      end
    end

    def change_status status, issue
      if issue.status != status
        old_status = issue.status
        issue.status = status
        return "changed status from #{old_status} to #{status}"
      end
      nil
    end

    def group_by_time(changelog)
      rv = {}
      changelog.each do |log|
        t = log[0].to_time
        rv[t] = [] unless rv[t]
        rv[t] << log
      end
      rv.sort
    end

    def update_issues( pairs, trac )
      pairs.each do |ticket, issue|
        puts "Working on #{ticket.id}/#{issue.id[0,4]}"
        issue_last_updated = if issue.log_events.size == 0
                               issue.creation_time
                             else
                               issue.log_events[-1][0]
                             end
        cl = trac.tickets.changelog( ticket.id )
        puts "Got #{cl.length} changes from Trac"
        puts "Ditz issue last changed #{issue_last_updated}"
        group_by_time( cl ).each do |time, array_of_changes|
          puts "Trac changegroup @ #{time}"
          if time > issue_last_updated
            whats = []
            comment = nil
            array_of_changes.each do |new_change|
              case new_change[2]
              when "comment"
                comment = new_change[2]
              when "description"
                if new_change[3] != issue.description
                  whats << "edited description"
                  issue.desc = new_change[3]
                end
              when "milestone"
                if new_change[3] != issue.release
                  whats << "assigned to release #{new_change[3]} from #{issue.release || 'unassigned'}"
                  issue.release = new_change[3]
                end
              when "owner"
                whats << change_status( :in_progress, issue )
              when "resolution"
                RES_DISPO
                issue.disposition = RES_DISPO[ new_change[3] ]
              when "status"
                whats << change_status( TSTATUS_DSTATUS[ new_change[3] ], issue )
              when "type"
                new_t = TTYPE_DTYPE[ new_change[3] ]
                whats << "type changed to #{new_t} from #{issue.type}"
                issue.type = new_t

              when "attachment", "cc", "component", "os", "priority", 
                "severity", "version"
                # NOOP
              else
                # NOOP
              end
            end
            # All changes were at the same time, and therefore by the same person
            issue.log_at( time, whats.join(", "), array_of_changes[0][1], comment )
          end
        end
      end
    end
  end

  class Config
    field :trac_sync_url, :prompt => "URL of Trac project (without the login/xmlrpc)"
    field :trac_sync_user, :prompt => "The Trac user ID that has XMLRPC permissions"
    field :trac_sync_pass, :prompt => "The Trac password for the account"
  end

  class Operator
    operation :sync, "Sync with a Trac repository"
    def sync( project, config )
      unless config.trac_sync_url
        STDERR.puts( "Please run 'ditz reconfigure' and set the Trac URL" )
        return
      end
      unless config.trac_sync_user
        STDERR.puts( "Please run 'ditz reconfigure' and set the Trac XMLRPC user name" )
        return
      end
      unless config.trac_sync_pass
        STDERR.puts( "Please run 'ditz reconfigure' and set the Trac XMLRPC password" )
        return
      end
      trac = Trac.new(config.trac_sync_url, config.trac_sync_user, config.trac_sync_pass)
      util = Ditz::TracUtil.new( project, config, trac )

      tickets = trac.tickets.get_all.values
      changelogs = []

      pairs = util.pair(tickets)
      util.create_issues( pairs.find_all {|m| m[1] == nil} )
      pairs = util.pair(tickets)
      util.update_issues( pairs.find_all {|m| m[0] != nil && m[1] != nil}, trac )
      #util.create_tickets( pairs.find_all {|m| m[0] == nil} )
      #issue.log "commented", config.user, comment
    end
  end
end


    



# ======================================  ======================================
# Ditz                                    Trac
# ======================================  ======================================
# Tickets
# id                                      id
# reporter                                reporter               
# creation_time                           time                   
# title                                   summary                
# type                                    type                   
# desc                                    description            
# release                                 milestone              
# component                               component              
# status                                  status                 
# ???                                     comments               
# disposition                             resolution
# references                              ???                    
# N/A                                     keywords               
# N/A                                     operating system       
# N/A                                     priority               
# N/A                                     owner                  
# N/A                                     cc                     
# N/A                                     attachments
# N/A                                     version
# N/A                                     severity
# N/A                                     changetime
#
# Releases
# release                                 name
#                                         due_date
# status                                  completed
# release_time                            completion date
#                                         description  
