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

require 'redmine/scm/adapters/abstract_adapter'
require 'rexml/document'
require "P4"

module Redmine
  module Scm
    module Adapters
      class PerforceAdapter < AbstractAdapter

        # whether we sould be supporting extended
        #  revision info on the revision #rev/head (ie. #1/7
        def supports_extended_revision?
          true
        end

        # Get info about the P4 repo
        def info

          path = fix_url(url)
          p4 = P4.new
          begin
            p4.port = root_url
            p4.user = @login
            if (!@password.nil? && !@password.empty?)
              p4.password = @password
            end

            p4.connect

            # get latest change for the depot spec
            h = p4.run_changes("-m 1 -s submitted", "#{path}...").shift
            change = P4Change.new( h )

            info = Info.new({:root_url => url,
                :lastrev => Revision.new({
                    :identifier => change.change,
                    :time => change.time,
                    :author => change.user.downcase
                  })
              })
            return info
          end
        rescue P4Exception
          p4.errors.each { |e| logger.error("Error executing [p4 changes -m 1 -s submitted #{path}...]:\n #{e}") }
        rescue Exception => e
          logger.error("Error executing [p4 changes -m 1 -s submitted #{path}...]: #{e.message}")
        ensure
          p4.disconnect
        end

        # Returns an Entries collection
        # or nil if the given path doesn't exist in the repository
        def entries(path=nil, identifier=nil)
          path = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}")
          path = fix_url(path)

          p4 = P4.new
          begin
            entries = Entries.new
            begin
              p4.port = root_url
              p4.user = @login
              if (!@password.nil? && !@password.empty?)
                p4.password = @password
              end
              p4.connect
              p4.run_dirs( path + "*" ).each do
                |directory|

                directory = directory[ "dir" ]
                dirname = entry_name(directory)
                dirpath = relative_path(directory)

                h = p4.run_changes("-m 1 -s submitted",  directory + "/...").shift
                change = P4Change.new( h )

                entries << Entry.new({:name => dirname,
                    :path => dirpath,
                    :kind => 'dir',
                    :size => nil,
                    :lastrev => Revision.new({
                        :identifier => change.change,
                        :time => change.time,
                        :author => change.user.downcase
                      })
                  })
              end
            rescue P4Exception
              p4.errors.each { |e| logger.error("Error executing [p4 dirs #{path}*]:\n #{e}") }
            end

            begin
            #run filelog to get files for change
            p4.run_filelog( path + "*" ).each do
                |depotfile|

                # newest one
                rev = depotfile.revisions[0]
                pathname = relative_path(rev.depot_file)
                name = entry_name(rev.depot_file)

                # iff deleted skip it
                next if rev.action == "delete"
                entries << Entry.new({:name => name,
                    :path => pathname,
                    :kind => 'file',
                    :size => rev.filesize.to_i,
                    :lastrev => Revision.new({
                        :identifier => rev.change,
                        :time => rev.time,
                        :author => (rev.user ? rev.user.downcase : "")
                      })
                  })
              end
            rescue P4Exception
              p4.errors.each { |e| logger.error("Error executing [p4 filelog #{path}*]:\n #{e}") }
            end

          rescue Exception => e
            logger.error("Error creating entries: #{e.message}")
          ensure
            p4.disconnect
          end

          logger.debug("Found #{entries.size} entries in the repository for #{path}") if logger && logger.debug?
          entries.sort_by_name
        end

        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
          path = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}")

          p4 = P4.new
          begin
            revisions = Revisions.new
            p4.port = root_url
            p4.user = @login
            if (!@password.nil? && !@password.empty?)
              p4.password = @password
            end
            p4.connect

            identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ""
            identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1

            opts = "-m 10 -s submitted"
            spec = path

            if identifier_from.is_a?(Integer)
              changecount = identifier_from - identifier_to
              opts = "-m " + changecount.to_s + " -s submitted"
              spec = path + "...@#{identifier_from}"
            end

            changenum = nil
            p4.run_changes("-L", opts, spec).each do
              |changehash|
              # describe the change
              changenum = changehash[ "change" ]
              describehash = p4.run("describe", "-s", changenum).shift
              # create the change
              change = P4Change.new( describehash )
              # load files into the change
              if ( describehash.has_key?( "depotFile" ) )
                describehash[ "depotFile" ].each_index do
                  |i|
                  name = describehash[ "depotFile"	][ i ]
                  type = describehash[ "type"	][ i ]
                  rev	 = describehash[ "rev" ][ i ]
                  act	 = describehash[ "action"	][ i ]
                  # create change file
                  p4chg = P4ChangeFile.new(name)
                  p4chg.type = type
                  p4chg.revno = rev.to_i
                  p4chg.action = act

                  # get head revision if needed
                  if ( p4chg.revno > 1 )
                    flog = p4.run_filelog( p4chg.depot_file ).shift
                    p4chg.head = flog.revisions.length
                  end

                  change.files.push( p4chg )
                end
              end

              revisions << Revision.new({:identifier => change.change,
                  :author => change.user.downcase,
                  :time => change.time,
                  :message => change.desc,
                  :paths => change.files
                })
            end
          rescue P4Exception
            p4.errors.each { |e| logger.error("Error executing [p4 describe -s #{changenum}]:\n #{e}") }
          rescue Exception => e
            logger.error("Error executing [p4 describe -s #{changenum}]: #{e.message}")
          ensure
            p4.disconnect
          end
          # return
          revisions
        end

        def diff(path, identifier_from, identifier_to=nil, type="inline")
          spec1 = nil
          spec2 = nil
          diff = []
          
          begin
            fixedpath = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}")

            if identifier_to.nil?
              if(path.empty?)
                # this handles when we have NO path..meaning ALL paths in the 'identifier_from' change
                p4 = P4.new
                begin
                  p4.port = root_url
                  p4.user = @login
                  if (!@password.nil? && !@password.empty?)
                    p4.password = @password
                  end
                  p4.connect
                  
                  describehash = p4.run("describe", "-s", identifier_from).shift
                  change = P4Change.new( describehash )
                  change.load_files( describehash )
  
                  change.files.each do
                    |p4dfile|
                    # the specs to diff
                    spec1 = p4dfile.depot_file + '#' + p4dfile.revno.to_s
                    spec2 = p4dfile.depot_file + '#' + (p4dfile.revno.to_i <= 2 ? 1 : p4dfile.revno.to_i-1).to_s
  
                    # run diff
                    diff += p4diff( spec1, spec2, "@#{identifier_from}")
                  end
                rescue P4Exception
                  p4.errors.each { |e| logger.error("Error executing [p4 describe -s #{identifier_from}]:\n #{e}") }
                rescue Exception => e
                  logger.error("Error executing [p4 describe -s #{identifier_from}]: #{e.message}")
                ensure
                  p4.disconnect
                end
              else
                # this handles when we have a path..meaning just one path in the 'identifier_from' change
                # the specs to diff
                spec1 = fixedpath + ((identifier_from.to_i <= 2) ? "@#{identifier_from}" : "@#{identifier_from.to_i-1}")
                spec2 = fixedpath + "@#{identifier_from}"

                # run diff
                diff += p4diff( spec1, spec2, identifier_from )
              end
            elsif !identifier_to.nil?
              # this handles when we have a path and a 'identifier_to' change number..meaning change-to-change
              identifier_from = (identifier_from and identifier_from.to_i > 0) ? ("@#{identifier_from}") : ""
              identifier_to = (identifier_to and identifier_to.to_i > 0) ? ("@#{identifier_to}") : ""

              # the specs to diff
              spec2 = fixedpath + identifier_from
              spec1 = fixedpath + identifier_to

              # run diff
              diff += p4diff( spec1, spec2, identifier_from )
            end
          rescue Exception => e
            logger.error("Error performing diff on #{spec1} #{spec2}]: #{e.message}")
          end

          diff
        end

        def p4diff(spec1, spec2, change)
          diff = []
          p4 = P4.new
          begin
            # untagged execution
            p4.tagged = false
            p4.port = root_url
            p4.user = @login
            if (!@password.nil? && !@password.empty?)
              p4.password = @password
            end
            p4.connect

            p4.run("diff2", "-u", spec1, spec2).each do
              |elem|
              # normalize to '\n'
              elem.gsub(/\r\n?/, "\n");
              elem.split("\n").each do
                |line|
                # look for file identifier..if found replace date/time with change#
                diff << line.gsub(/\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}/, "#{change}")
              end
            end
          rescue P4Exception
            p4.errors.each { |e| logger.error("Error executing [p4 diff2 -u #{spec1} #{spec2}]:\n #{e}") }
          rescue Exception => e
            logger.error("Error executing [p4 diff2 -u #{spec1} #{spec2}]: #{e.message}")
          ensure
            p4.disconnect
          end
          diff
        end

        def cat(path, identifier=nil)
          path = (path.blank? ? "#{fix_url(url)}" : "#{fix_url(url)}#{relative_path(path)}")

          cat = nil
          p4 = P4.new
          begin
            # untagged execution
            p4.tagged = false
            p4.port = root_url
            p4.user = @login
            if (!@password.nil? && !@password.empty?)
              p4.password = @password
            end
            p4.connect

            identifier = (identifier and identifier.to_i > 0) ? ("@#{identifier}") : ""
            spec = path + identifier
            cat = p4.run_print("-q", spec)
            cat = cat.to_s
          rescue P4Exception
            p4.errors.each { |e| logger.error("Error executing [p4 print -q #{spec}]:\n #{e}") }
          rescue Exception => e
            logger.error("Error executing [p4 print -q #{spec}]: #{e.message}")
          ensure
            p4.disconnect
          end
          cat
        end

        # Returns just the name of the entry from the depot spec
        def entry_name(path)
          @entry_name = ( path.include?('/') ? path[1+path.rindex('/')..-1] : path )
        end

        # Returns a path relative to the depotspec(url) of the repository
        def relative_path(path)
          @relative_path = path.gsub(fix_url(url), '')
        end

        # Make sure there is a begining '//' and an ending '/', also remove trailing ...
        def fix_url(path)
          path = (path.starts_with?("//") ? path : "//" + path)
          path = path.gsub('/...', '')
          @fix_url = (path.ends_with?("/") ? path : path + "/")
        end
      end

      class P4Change
        # Constructor. Pass the hash returned by P4#run_describe( "-s" ) in
        # tagged mode.
        def initialize( hash )
          @change = hash[ "change"  ]
          @user	= hash[ "user"    ]
          @client = hash[ "client"  ]
          @desc	= hash[ "desc"    ]
          @time 	= Time.at( hash[ "time" ].to_i )

          @status	= hash[ "status"  ]
          @files	= Array.new
          @jobs = Hash.new

          if ( hash.has_key?( "job" ) )
            hash[ "job" ].each_index do
              |i|
              job 	= hash[ "job" 	  ][ i ]
              status	= hash[ "jobstat" ][ i ]
              @jobs[ job ] = status
            end
          end
        end

        attr_reader :change, :user, :client, :desc, :time, :status, :files, :jobs
        attr_writer :files

        # Shorthand iterator for looking at the files in the change
        def each_file( &block )
          @files.each { |f| yield( f ) }
        end

        # Shorthand iterator for looking at the jobs fixed by the change
        def each_job( &block )
          @jobs.each { |j| yield( j ) }
        end

        def load_files( hash )
          if ( hash.has_key?( "depotFile" ) )
            hash[ "depotFile" ].each_index do
              |i|
              name = hash[ "depotFile"	][ i ]
              type = hash[ "type"	][ i ]
              rev	 = hash[ "rev" ][ i ]
              act	 = hash[ "action"	][ i ]
              # create change file
              p4chg = P4ChangeFile.new(name)
              p4chg.type = type
              p4chg.revno = rev.to_i
              p4chg.action = act

              @files.push( p4chg )
            end
          end
        end
      end

      class P4ChangeFile
        def initialize( depotfile )
          @depot_file = depotfile
          @revno
          @type
          @action
          @head = 1
        end

        attr_reader :depot_file
        attr_accessor :revno, :type, :head, :action
      end
    end
  end
end
