require "grit"

class GraphCommit
  attr_accessor :time, :space
  attr_accessor :refs

  def self.to_graph(project)
    @repo = project.repo
    commits = Grit::Commit.find_all(@repo, nil, {:max_count => 650})

    ref_cache = {}

    commits.map! {|c| GraphCommit.new(Commit.new(c))}
    commits.each { |commit| commit.add_refs(ref_cache, @repo) }

    days = GraphCommit.index_commits(commits)
    @days_json = days.compact.collect{|d| [d.day, d.strftime("%b")] }.to_json
    @commits_json = commits.map(&:to_graph_hash).to_json

    return @days_json, @commits_json
  end

  # Method is adding time and space on the
  # list of commits. As well as returns date list
  # corelated with time set on commits.
  #
  # @param [Array<GraphCommit>] comits to index
  #
  # @return [Array<TimeDate>] list of commit dates corelated with time on commits
  def self.index_commits(commits)
    days, heads = [], []
    map = {}

    commits.reverse.each_with_index do |c,i|
      c.time = i
      days[i] = c.committed_date
      map[c.id] = c
      heads += c.refs unless c.refs.nil?
    end

    heads.select!{|h| h.is_a? Grit::Head or h.is_a? Grit::Remote}
    # sort heads so the master is top and current branches are closer
    heads.sort! do |a,b|
      if a.name == "master"
        -1
      elsif b.name == "master"
        1
      else
        b.commit.committed_date <=> a.commit.committed_date
      end
    end

    @_reserved = {}
    days.each_index do |i|
      @_reserved[i] = []
    end

    heads.each do |h|
      if map.include? h.commit.id then
        place_chain(map[h.commit.id], map)
      end
    end
    days
  end

  # Add space mark on commit and its parents
  #
  # @param [GraphCommit] the commit object.
  # @param [Hash<String,GraphCommit>] map of commits
  def self.place_chain(commit, map, parent_time = nil)
    leaves = take_left_leaves(commit, map)
    if leaves.empty? then
      return
    end
    space = find_free_space(leaves.last.time..leaves.first.time)
    leaves.each{|l| l.space = space}
    # and mark it as reserved
    min_time = leaves.last.time
    parents = leaves.last.parents.collect
    parents.each do |p|
      if map.include? p.id then
        parent = map[p.id]
        if parent.time < min_time then
          min_time = parent.time
        end
      end
    end
    if parent_time.nil? then
      max_time = leaves.first.time
    else
      max_time = parent_time - 1
    end
    mark_reserved(min_time..max_time, space)
    # Visit branching chains
    leaves.each do |l|
      parents = l.parents.collect
        .select{|p| map.include? p.id and map[p.id].space == 0}
      for p in parents
        place_chain(map[p.id], map, l.time)
      end
    end
  end

  def self.mark_reserved(time_range, space)
    for day in time_range
      @_reserved[day].push(space)
    end
  end

  def self.find_free_space(time_range)
    reserved = []
    for day in time_range
        reserved += @_reserved[day]
    end
    space = 1
    while reserved.include? space do
      space += 1
    end
    space
  end

  # Takes most left subtree branch of commits
  # which don't have space mark yet.
  #
  # @param [GraphCommit] the commit object.
  # @param [Hash<String,GraphCommit>] map of commits
  #
  # @return [Array<GraphCommit>] list of branch commits
  def self.take_left_leaves(commit, map)
    leaves = []
    leaves.push(commit)  if commit.space == 0
    while true
      parent = commit.parents.collect
        .select{|p| map.include? p.id and map[p.id].space == 0}
      if parent.count == 0 then
        return leaves
      else
        commit = map[parent.first.id]
        leaves.push(commit)
      end
    end
  end


  def initialize(commit)
    @_commit = commit
    @time = -1
    @space = 0
  end

  def method_missing(m, *args, &block)
    @_commit.send(m, *args, &block)
  end

  def to_graph_hash
    h = {}
    h[:parents] = self.parents.collect do |p|
      [p.id,0,0]
    end
    h[:author]  = Gitlab::Encode.utf8(author.name)
    h[:time]    = time
    h[:space]   = space
    h[:refs]    = refs.collect{|r|r.name}.join(" ") unless refs.nil?
    h[:id]      = sha
    h[:date]    = date
    h[:message] = Gitlab::Encode.utf8(message)
    h[:login]   = author.email
    h
  end

  def add_refs(ref_cache, repo)
    if ref_cache.empty?
      repo.refs.each do |ref|
        ref_cache[ref.commit.id] ||= []
        ref_cache[ref.commit.id] << ref
      end
    end
    @refs = ref_cache[@_commit.id] if ref_cache.include?(@_commit.id)
    @refs ||= []
  end
end
