#coding=utf-8
#
require 'redmine/scm/adapters/abstract_adapter'
require 'base64'

module Redmine
  module Scm
    module Adapters

      class GitlabAdapter < AbstractAdapter

        class GitBranch < Branch 
          attr_accessor :is_default
        end

        def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil, project_id)
          super
          @g = Gitlab.client
          r = Repository.where("url =? and project_id =?", url, project_id).first
          @project = r.project.gpid
          # @project = Repository.find_by_url(url).project.gpid
          @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
        end

        def path_encoding
          @path_encoding
        end

        def info
          begin
            Info.new(:root_url => url, :lastrev => lastrev('',nil))
          rescue
            nil
          end
        end

        def branches
          return @branches if @branches
          @branches = []
          branches = @g.branches(@project)
          branches.each do |line|
              name = line.name
              scmid = line.commit.id
              bran = GitBranch.new(name)
              bran.revision =  scmid
              bran.scmid    =  name
              bran.is_default = true  #TODO
              @branches << bran
          end
          @branches.sort!
        rescue ScmCommandAborted
          nil
        end

        def tags
          return @tags if @tags
          tags = @g.tags(@project)
          @tags = []
          tags.each do |tag|
            @tags << tag.name
          end
        rescue ScmCommandAborted
          nil
        end

        def default_branch
          bras = self.branches
          return nil if bras.nil?
          default_bras = bras.select{|x| x.is_default == true}
          return default_bras.first.to_s if ! default_bras.empty?
          master_bras = bras.select{|x| x.to_s == 'master'}
          master_bras.empty? ? bras.first.to_s : 'master' 
        end

        def entry(path=nil, identifier=nil)
          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
          search_path = parts[0..-2].join('/')
          search_name = parts[-1]
          if search_path.blank? && search_name.blank?
            # Root entry
            Entry.new(:path => '', :kind => 'dir')
          else
            # Search for the entry in the parent directory
            es = entries(search_path, identifier,
                         options = {:report_last_commit => false})
            es ? es.detect {|e| e.name == search_name} : nil
          end
        end

        def entries(path=nil, identifier=nil, options={})
          entries  = Entries.new
          trees = @g.trees(@project, path: path, ref_name: identifier)
          trees.each do |tree|
            entries << Entry.new({
              :name => tree.name,
              :path => File.join(path,tree.name),
              :kind => tree.type == 'tree' ? 'dir' : 'file',
              :size => nil,
              :lastrev =>  nil
            }) 
          end
          entries.sort_by_name
        rescue ScmCommandAborted
          nil
        rescue Exception
          nil
        end

        def lastrev(path, rev)
          return nil if path.nil?
          cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
          cmd_args << rev if rev
          cmd_args << "--" << path unless path.empty?
          lines = []
          git_cmd(cmd_args) { |io| lines = io.readlines }
          begin
            id = lines[0].split[1]
            author = lines[1].match('Author:\s+(.*)$')[1]
            time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])

            Revision.new({
              :identifier => id,
              :scmid      => id,
              :author     => author,
              :time       => time,
              :message    => nil,
              :paths      => nil
            })
          rescue NoMethodError => e
            logger.error("The revision '#{path}' has a wrong format")
            return nil
          end
        rescue ScmCommandAborted
          nil
        end

        def revisions(path, identifier_from, identifier_to, options={})
          revs = Revisions.new
          cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin|
          cmd_args << "--reverse" if options[:reverse]
          cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
          cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
          revisions = []
          if identifier_from || identifier_to
            revisions << ""
            revisions[0] << "#{identifier_from}.." if identifier_from
            revisions[0] << "#{identifier_to}" if identifier_to
          else
            unless options[:includes].blank?
              revisions += options[:includes]
            end
            unless options[:excludes].blank?
              revisions += options[:excludes].map{|r| "^#{r}"}
            end
          end


          commits = @g.commits(@project, {ref_name: identifier_to})
          commits.each do |commit|
            revision = Revision.new({
              :identifier => commit.id,
              :scmid      => commit.id,
              :author     => commit.author_name,
              :time       => Time.parse(commit.created_at),
              :message    => commit.message,
              :paths      => nil,
              :parents    => nil
            })
            revs << revision
          end

          revs
        rescue ScmCommandAborted => e
          err_msg = "git log error: #{e.message}"
          logger.error(err_msg)
          if block_given?
            raise CommandFailed, err_msg
          else
            revs
          end
        end

        def diff(path, identifier_from, identifier_to=nil)
          path ||= ''
          cmd_args = []
          if identifier_to
            cmd_args << "diff" << "--no-color" <<  identifier_to << identifier_from
          else
            cmd_args << "show" << "--no-color" << identifier_from
          end
          cmd_args << "--" <<  scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
          diff = []
          git_cmd(cmd_args) do |io|
            io.each_line do |line|
              diff << line
            end
          end
          diff
        rescue ScmCommandAborted
          nil
        end

        def annotate(path, identifier=nil)
          identifier = 'HEAD' if identifier.blank?
          cmd_args = %w|blame|
          cmd_args << "-p" << identifier << "--" <<  scm_iconv(@path_encoding, 'UTF-8', path)
          blame = Annotate.new
          content = nil
          git_cmd(cmd_args) { |io| io.binmode; content = io.read }
          # git annotates binary files
          return nil if content.is_binary_data?
          identifier = ''
          # git shows commit author on the first occurrence only
          authors_by_commit = {}
          content.split("\n").each do |line|
            if line =~ /^([0-9a-f]{39,40})\s.*/
              identifier = $1
            elsif line =~ /^author (.+)/
              authors_by_commit[identifier] = $1.strip
            elsif line =~ /^\t(.*)/
              blame.add_line($1, Revision.new(
                :identifier => identifier,
                :revision   => identifier,
                :scmid      => identifier,
                :author     => authors_by_commit[identifier]
              ))
              identifier = ''
              author = ''
            end
          end
          blame
        rescue ScmCommandAborted
          nil
        end

        def cat(path, identifier=nil)
          if identifier.nil?
            identifier = 'HEAD'
          end
          file = @g.files(@project, path, identifier)
          cat = Base64.decode64 file.content
        rescue ScmCommandAborted
          nil
        end

        def parse_commit(commits)
          sum = {file: 0, insertion: 0, deletion: 0}
          commits.split("\n").each do |commit|
            if /(\d+)\s+?file/ =~ commit
              sum[:file] += $1 .to_i
            end
            if /(\d+)\s+?insertion/ =~ commit
              sum[:insertion] += $1.to_i 
            end
            if /(\d+)\s+?deletion/ =~ commit
              sum[:deletion] += $1.to_i
            end
          end
          sum[:insertion] + sum[:deletion]
        end

        def commits(authors, start_date, end_date, branch='master')
          rs = []
          authors.each do |author|
            cmd_args = %W|log #{branch} --pretty=tformat: --shortstat --author=#{author} --since=#{start_date} --until=#{end_date}|
            commits = ''
            git_cmd(cmd_args) do |io|
              commits = io.read
            end
            logger.info "git log output for #{author} #{commits}"
            rs << {author: author, num: parse_commit(commits)}
          end
          rs
        end

        class Revision < Redmine::Scm::Adapters::Revision
          # Returns the readable identifier
          def format_identifier
            identifier[0,8]
          end
        end

        def git_cmd(args, options = {}, &block)
          logger.info "git cmd: #{args.join(' ')}"
        end
        private :git_cmd
      end

    end
  end
end
