# Redmine - project management software
# Copyright (C) 2006-2011  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.

# Portions of this code adapted from Michael Vance, see:
#  http://www.redmine.org/issues/339
# Adapted by Terry Suereth.

require 'redmine/scm/adapters/abstract_adapter'
require 'uri'

module Redmine
  module Scm
    module Adapters
      class PerforceAdapter < AbstractAdapter

        # P4 executable name
        P4_BIN = Redmine::Configuration['scm_perforce_command'] || "p4"

        class << self
          def client_command
            @@bin    ||= P4_BIN
          end

          def sq_bin
            @@sq_bin ||= shell_quote_command
          end

          def client_version
            @@client_version ||= (p4_binary_version || [])
          end

          def client_available
            !client_version.empty?
          end

          def p4_binary_version
            scm_version = scm_version_from_command_line.dup
            bin_version = ''
            if scm_version.respond_to?(:force_encoding)
              scm_version.force_encoding('ASCII-8BIT')
            end
            if m = scm_version.match(%r{Rev\. P4/[^/]+/([^/]+)/})
              bin_version = m[1].scan(%r{\d+}).collect(&:to_i)
            end
            bin_version
          end

          def scm_version_from_command_line
            shellout("#{sq_bin} -V") { |io| io.read }.to_s
          end
        end

        # Get info about the p4 repository
        def info
          cmd = "#{self.class.sq_bin}"
          cmd << credentials_string
          cmd << " changes -m 1 -s submitted -t "
          cmd << shell_quote(depot)
          info = nil
          shellout(cmd) do |io|
            io.each_line do |line|
              change = parse_change(line)
              next unless change
              begin
                info = Info.new({:root_url => url,
                                 :lastrev => Revision.new({
                                   :identifier => change[:id],
                                   :author => change[:author],
                                   :time => change[:time],
                                   :message => change[:desc]
                                 })
                               })
              rescue
              end
            end
          end
          return nil if $? && $?.exitstatus != 0
          info
        rescue CommandFailed
          return nil
        end

        # Returns an Entries collection
        # or nil if the given path doesn't exist in the repository
        def entries(path=nil, identifier=nil, options={})
          query_path = path.empty? ? "#{depot_no_dots}" : "#{depot_no_dots}#{path}"
          query_path.chomp!
          query_path = query_path.gsub(%r{\/\Z}, '') + "/*"
          identifier = (identifier and identifier.to_i > 0) ? "@#{identifier}" : nil
          entries = Entries.new

          p4login = credentials_string

          # Dirs
          cmd = "#{self.class.sq_bin}"
          cmd << p4login
          cmd << " dirs "
          cmd << shell_quote(query_path)
          cmd << "#{identifier}" if identifier
          # <path>/* - no such file(s).
          # -or-
          # <path>
          shellout(cmd) do |io|
            io.each_line do |line|
              # TODO this is actually unnecessary as the cmd will
              # write to stderr, not stdin, so we'll never even get
              # to this line
              next if line =~ %r{ - no such file\(s\)\.$}
              full_path = line.gsub(Regexp.new("#{Regexp.escape(depot_no_dots)}"), '')
              full_path.chomp!
              name = full_path.split("/").last()
              entries << Entry.new({
                                     :name => name,
                                     :path => full_path,
                                     :kind => 'dir',
                                     :size => nil,
                                     :lastrev => make_revision(p4login, full_path + "/...", identifier)
                                   })
            end
          end

          # Files
          cmd = "#{self.class.sq_bin}"
          cmd << p4login
          cmd << " files "
          cmd << shell_quote(query_path)
          cmd << "#{identifier}" if identifier
          # <path>#<n> - <action> change <n> (<type>)
          shellout(cmd) do |io|
            io.each_line do |line|
              next unless line =~ %r{(.+)#(\d+) - (\S+) change (\d+) \((.+)\)}
              full_path = $1
              action = $3
              id = $4
              next if action == 'delete'
              fixed_path = full_path.gsub(Regexp.new("#{Regexp.escape(depot_no_dots)}"), '')
              fixed_path.chomp!
              name = fixed_path.split("/").last()
              size = nil

              subcmd = "#{self.class.sq_bin}"
              subcmd << p4login
              subcmd << " fstat -Ol "
              subcmd << shell_quote(full_path)
              shellout(subcmd) do |subio|
                subio.each_line do |subline|
                  next unless subline =~ %r{\.\.\. fileSize (\d+)}
                  size = $1
                end
              end

              entries << Entry.new({
                                     :name => name,
                                     :path => fixed_path,
                                     :kind => 'file',
                                     :size => size,
                                     :lastrev => make_revision(p4login, fixed_path, identifier)
                                   })
            end
          end

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

        def properties(path, identifier=nil)
          return nil
        end

        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
          base_path = path.empty? ? "#{depot_no_dots}" : "#{depot_no_dots}#{path}"
          base_path.chomp!
          base_path = base_path.gsub(%r{\/\Z}, '')
          # We don't know if 'path' is a file or directory, and p4 has different syntax requirements for each --
          #  luckily, we can try both at the same time, and one will work while the other gets effectively ignored.
          query_path_file = base_path
          query_path_dir = query_path_file + "/..."
          # options[:reverse] doesn't make any sense to Perforce
          identifer_from = nil if options[:all]
          identifier_to = nil if options[:all]
          identifier_from = (identifier_from and identifier_from.to_i > 0) ? "@#{identifier_from}" : nil
          identifier_to = (identifier_to and identifier_to.to_i > 0) ? "@#{identifier_to}" : nil
          revisions = Revisions.new

          p4login = credentials_string

          cmd = "#{self.class.sq_bin}"
          cmd << p4login
          cmd << " changes -t "
          cmd << "-m #{options[:limit]} " if options[:limit]
          cmd << shell_quote(query_path_file)
          cmd << "#{identifier_to}," if identifier_to
          cmd << "#{identifier_from}" if identifier_from
          cmd << " "
          cmd << shell_quote(query_path_dir)
          cmd << "#{identifier_to}," if identifier_to
          cmd << "#{identifier_from}" if identifier_from
          shellout(cmd) do |io|
            io.each_line do |line|
              change = parse_change(line)
              next unless change
              full_desc = ''
              paths = []
              subcmd = "#{self.class.sq_bin}"
              subcmd << p4login
              subcmd << " describe -s #{change[:id]}"
              shellout(subcmd) do |subio|
                subio.each_line do |subline|
                  if subline =~ %r{\AChange #{change[:id]}}
                    next
                  elsif subline =~ %r{\AAffected files \.\.\.}
                    next
                  elsif subline =~ %r{\A\.\.\. (.+)#(\d+) (\S+)}
                    if options[:with_paths]
                      subpath = $1
                      revision = $2
                      action_full = $3
                      next if subpath !~ %r{^#{Regexp.escape(base_path)}}
                      case
                        when action_full == 'add'
                          action = 'A'
                        when action_full == 'edit'
                          action = 'M'
                        when action_full == 'delete'
                          action = 'D'
                        when action_full == 'branch'
                          action = 'C'
                        when action_full == 'import'
                          action = 'A'
                        when action_full == 'integrate'
                          action = 'M'
                        else
                          action = 'A' # FIXME: best guess, it's a new file?
                      end
                      fixed_path = subpath.gsub(Regexp.new("#{Regexp.escape(depot_no_dots)}"), '')
                      paths << {:action => action, :path => fixed_path, :revision => revision}
                    end
                  else
                    full_desc << subline
                  end
                end
              end
              revisions << Revision.new({
                                          :identifier => change[:id],
                                          :author => change[:author],
                                          :time => change[:time],
                                          :message => full_desc.empty? ? change[:desc] : full_desc,
                                          :paths => paths
                                        })
            end
          end
          return nil if revisions.empty?
          revisions
        end

        def diff(path, identifier_from, identifier_to=nil)
          base_path = path.empty? ? "#{depot_no_dots}" : "#{depot_no_dots}#{path}"
          base_path.chomp!
          base_path = base_path.gsub(%r{\/\Z}, '')
          # We don't know if 'path' is a file or directory, and p4 has different syntax requirements for each.
          query_path_file = base_path
          query_path_dir = query_path_file + "/..."

          identifier_to = (identifier_to and identifier_to.to_i > 0) ? "@#{identifier_to}" : "@#{identifier_from.to_i - 1}"
          identifier_from = (identifier_from and identifier_from.to_i > 0) ? "@#{identifier_from}" : "#head"

          p4login = credentials_string

          diff = []

          # File
          cmd = "#{self.class.sq_bin}"
          cmd << p4login
          cmd << " diff2 -du "
          cmd << shell_quote(query_path_file)
          cmd << "#{identifier_to} "
          cmd << shell_quote(query_path_file)
          cmd << "#{identifier_from}"
          shellout(cmd) do |io|
            io.each_line do |line|
              next if line =~ %r{ - no such file\(s\)\.$}
              next if line =~ %r{\A====.+==== identical\Z}

              if line =~ %r{\A==== (.+) - (.+) ==== ?(.*)}
                file1 = $1
                file2 = $2
                action = $3
                filename = file1
                if(file1 =~ %r{<\s*none\s*>})
                  filename = file2
                end
                filename = filename.gsub(%r{ \(.*\)\Z}, '') # remove filetype declaration
                filename = filename.gsub(%r{\#\d+\Z}, '') # remove file revision

                diff << "Index: #{filename}"
                diff << "==========================================================================="
                diff << "--- #{filename}#{identifier_to}"
                diff << "+++ #{filename}#{identifier_from}"
              else
                diff << line
              end
            end
          end

          # Dir
          cmd = "#{self.class.sq_bin}"
          cmd << p4login
          cmd << " diff2 -du "
          cmd << shell_quote(query_path_dir)
          cmd << "#{identifier_to} "
          cmd << shell_quote(query_path_dir)
          cmd << "#{identifier_from}"
          shellout(cmd) do |io|
            io.each_line do |line|
              next if line =~ %r{ - no such file\(s\)\.$}
              next if line =~ %r{\A====.+==== identical\Z}

              if line =~ %r{\A==== (.+) - (.+) ==== ?(.*)}
                file1 = $1
                file2 = $2
                action = $3
                filename = file1
                if(file1 =~ %r{<\s*none\s*>})
                  filename = file2
                end
                filename = filename.gsub(%r{ \(.*\)\Z}, '') # remove filetype declaration
                filename = filename.gsub(%r{\#\d+\Z}, '') # remove file revision

                diff << "Index: #{filename}"
                diff << "==========================================================================="
                diff << "--- #{filename}#{identifier_to}"
                diff << "+++ #{filename}#{identifier_from}"
              else
                diff << line
              end
            end
          end

          return nil if diff.empty?
          diff
        end

        def cat(path, identifier=nil)
          return nil if path.empty?
          query_path = "#{depot_no_dots}#{path}"
          cmd = "#{self.class.sq_bin}"
          cmd << credentials_string
          cmd << " print -q "
          cmd << shell_quote(query_path)
          cat = nil
          shellout(cmd) do |io|
            io.binmode
            cat = io.read
          end
          return nil if $? && $?.exitstatus != 0
          cat
        end

        def annotate(path, identifier=nil)
          return nil if path.empty?
          query_path = "#{depot_no_dots}#{path}"
          cmd = "#{self.class.sq_bin}"
          cmd << credentials_string
          cmd << " annotate -q -c "
          cmd << shell_quote(query_path)
          blame = Annotate.new
          shellout(cmd) do |io|
            io.each_line do |line|
              # <n>: <line>
              next unless line =~ %r{(\d+)\:\s(.*)$}
              id = $1
              rest = $2
              blame.add_line(rest.rstrip, Revision.new(:identifier => id))
            end
          end
          return nil if $? && $?.exitstatus != 0
          blame
        end

        private

        def credentials_login
          return nil unless !@login.blank? && !@password.blank?

          File.open("/tmp/perforce_adapter_login", 'w') { |f| f.write(@password) }
          ticket = shellout("#{self.class.sq_bin} -p #{shell_quote(@root_url)} -u #{shell_quote(@login)} login -p </tmp/perforce_adapter_login 2>/dev/null") { |io| io.read }.to_s
          File.delete("/tmp/perforce_adapter_login")
          if ticket.respond_to?(:force_encoding)
            ticket.force_encoding('ASCII-8BIT')
          end

          str = " -P "
          if m = ticket.match(%r/[0-9A-Za-z]{32}/)
            str << "#{shell_quote(m[0])}"
          else
            str << "#{shell_quote(@password)}"
          end
          str
        end

        def credentials_string
          str = ''
          str << " -p #{shell_quote(@root_url)}"
          str << " -u #{shell_quote(@login)}" unless @login.blank?
          str << credentials_login
          str
        end

        def depot
          url
        end

        def depot_no_dots
          url.gsub(Regexp.new("#{Regexp.escape('...')}$"), '')
        end

        def parse_change(line)
          # Change <n> on <day> <time> by <user>@<client> '<desc>'
          return nil unless line =~ %r{Change (\d+) on (\S+) (\S+) by (\S+) '(.*)'$}
          id = $1
          day = $2
          time = $3
          author = $4
          desc = $5
          # FIXME: inefficient to say the least
          #cmd = "#{self.class.sq_bin} users #{author.split('@').first}"
          #shellout(cmd) do |io|
          #  io.each_line do |line|
          #    # <user> <<email>> (<name>) accessed <date>
          #    next unless line =~ %r{\S+ \S+ \((.*)\) accessed \S+}
          #    author = $1
          #  end
          #end
          author = author.split('@').first
          return {:id => id, :author => author, :time => Time.parse("#{day} #{time}").localtime, :desc => desc}
        end

        def make_revision(p4login, path, identifier)
          return nil if path.empty?
          identifier ||= ''
          query_path = "#{depot_no_dots}#{path}"

          cmd = "#{self.class.sq_bin}"
          cmd << p4login
          cmd << " changes -m 1 -s submitted -t "
          cmd << shell_quote(query_path)
          cmd << "#{identifier}" if identifier
          revisions = []
          shellout(cmd) do |io|
            io.each_line do |line|
              change = parse_change(line)
              next unless change
              revisions << Revision.new({
                                          :identifier => change[:id],
                                          :author => change[:author],
                                          :time => change[:time],
                                          :message => change[:desc]
                                        })
            end
          end
          return nil if revisions.empty?
          revisions.first
        end

      end
    end
  end
end
