require 'json'
require 'tempfile'
require 'version_sorter'
require 'rocco'
require 'docurium/version'
require 'docurium/layout'
require 'docurium/cparser'
require 'pp'
require 'rugged'
require 'redcarpet'

# Markdown expects the old redcarpet compat API, so let's tell it what
# to use
Rocco::Markdown = RedcarpetCompat

class Docurium
  attr_accessor :branch, :output_dir, :data

  def initialize(config_file)
    raise "You need to specify a config file" if !config_file
    raise "You need to specify a valid config file" if !valid_config(config_file)
    @sigs = {}
    @groups = {}
    repo_path = Rugged::Repository.discover('.')
    @repo = Rugged::Repository.new(repo_path)
    clear_data
  end

  def clear_data(version = 'HEAD')
    @data = {:files => [], :functions => {}, :globals => {}, :types => {}, :prefix => ''}
    @data[:prefix] = option_version(version, 'input', '')
  end

  def option_version(version, option, default = nil)
    if @options['legacy']
      if valhash = @options['legacy'][option]
        valhash.each do |value, versions|
          return value if versions.include?(version)
        end
      end
    end
    opt = @options[option]
    opt = default if !opt
    opt
  end

  def generate_docs
    out "* generating docs"
    output_index = Rugged::Index.new
    write_site(output_index)
    versions = get_versions
    versions << 'HEAD'
    versions.each do |version|
      out "  - processing version #{version}"
      index = @repo.index
      index.clear
      clear_data(version)
      read_subtree(index, version, @data[:prefix])
      parse_headers(index)
      tally_sigs(version)

      tf = File.expand_path(File.join(File.dirname(__FILE__), 'docurium', 'layout.mustache'))
      if ex = option_version(version, 'examples')
        if subtree = find_subtree(version, ex) # check that it exists
          index.read_tree(subtree)
          out "  - processing examples for #{version}"

          files = []
          index.each do |entry|
            next unless entry[:path].match(/\.c$/)
            files << entry[:path]
          end

          files.each do |file|
            out "    # #{file}"

            # highlight, roccoize and link
            rocco = Rocco.new(file, files, {:language => 'c'}) do
              ientry = index[file]
              blob = @repo.lookup(ientry[:oid])
              blob.content
            end
            rocco_layout = Rocco::Layout.new(rocco, tf)
            rocco_layout.version = version
            rf = rocco_layout.render

            extlen = -(File.extname(file).length + 1)
            rf_path = file[0..extlen] + '.html'
            rel_path = "ex/#{version}/#{rf_path}"

            # look for function names in the examples and link
            id_num = 0
            @data[:functions].each do |f, fdata|
              rf.gsub!(/#{f}([^\w])/) do |fmatch|
                extra = $1
                id_num += 1
                name = f + '-' + id_num.to_s
                # save data for cross-link
                @data[:functions][f][:examples] ||= {}
                @data[:functions][f][:examples][file] ||= []
                @data[:functions][f][:examples][file] << rel_path + '#' + name
                "<a name=\"#{name}\" class=\"fnlink\" href=\"../../##{version}/group/#{fdata[:group]}/#{f}\">#{f}</a>#{extra}"
              end
            end

            # write example to the repo
            sha = @repo.write(rf, :blob)
            output_index.add(:path => rel_path, :oid => sha, :mode => 0100644)

            @data[:examples] ||= []
            @data[:examples] << [file, rel_path]
          end
        end

        if version == 'HEAD'
          show_warnings
        end
      end

      sha = @repo.write(@data.to_json, :blob)
      output_index.add(:path => "#{version}.json", :oid => sha, :mode => 0100644)
    end

    project = {
      :versions => versions.reverse,
      :github   => @options['github'],
      :name     => @options['name'],
      :signatures => @sigs,
      :groups   => @groups
    }
    sha = @repo.write(project.to_json, :blob)
    output_index.add(:path => "project.json", :oid => sha, :mode => 0100644)

    br = @options['branch']
    out "* writing to branch #{br}"
    refname = "refs/heads/#{br}"
    tsha = output_index.write_tree(@repo)
    puts "\twrote tree   #{tsha}"
    ref = Rugged::Reference.lookup(@repo, refname)
    user = { :name => @repo.config['user.name'], :email => @repo.config['user.email'], :time => Time.now }
    options = {}
    options[:tree] = tsha
    options[:author] = user
    options[:committer] = user
    options[:message] = 'generated docs'
    options[:parents] = ref ? [ref.target] : []
    options[:update_ref] = refname
    csha = Rugged::Commit.create(@repo, options)
    puts "\twrote commit #{csha}"
    puts "\tupdated #{br}"
  end

  def show_warnings
    out '* checking your api'

    # check for unmatched paramaters
    unmatched = []
    @data[:functions].each do |f, fdata|
      unmatched << f if fdata[:comments] =~ /@param/
    end
    if unmatched.size > 0
      out '  - unmatched params in'
      unmatched.sort.each { |p| out ("\t" + p) }
    end

    # check for changed signatures
    sigchanges = []
    @sigs.each do |fun, data|
      if data[:changes]['HEAD']
        sigchanges << fun
      end
    end
    if sigchanges.size > 0
      out '  - signature changes in'
      sigchanges.sort.each { |p| out ("\t" + p) }
    end
  end

  def get_versions
    tags = []
    @repo.tags.each { |tag| tags << tag.gsub(%r(^refs/tags/), '') }
    VersionSorter.sort(tags)
  end

  def parse_headers(index)
    headers(index).each do |header|
      records = parse_header(index, header)
      update_globals(records)
    end

    @data[:groups] = group_functions
    @data[:types] = @data[:types].sort # make it an assoc array
    find_type_usage
  end

  private

  def tally_sigs(version)
    @lastsigs ||= {}
    @data[:functions].each do |fun_name, fun_data|
      if !@sigs[fun_name]
        @sigs[fun_name] ||= {:exists => [], :changes => {}}
      else
        if @lastsigs[fun_name] != fun_data[:sig]
          @sigs[fun_name][:changes][version] = true
        end
      end
      @sigs[fun_name][:exists] << version
      @lastsigs[fun_name] = fun_data[:sig]
    end
  end

  def find_subtree(version, path)
    tree = nil
    if version == 'HEAD'
      tree = @repo.lookup(@repo.head.target).tree
    else
      trg = @repo.lookup(Rugged::Reference.lookup(@repo, "refs/tags/#{version}").target)
      if(trg.class == Rugged::Tag)
        trg = trg.target
      end

      tree = trg.tree
    end

    begin
      tree_entry = tree.path(path)
      @repo.lookup(tree_entry[:oid])
    rescue Rugged::TreeError
      nil
    end
  end

  def read_subtree(index, version, path)
    tree = find_subtree(version, path)
    index.read_tree(tree)
  end

  def valid_config(file)
    return false if !File.file?(file)
    fpath = File.expand_path(file)
    @project_dir = File.dirname(fpath)
    @config_file = File.basename(fpath)
    @options = JSON.parse(File.read(fpath))
    !!@options['branch']
  end

  def group_functions
    func = {}
    @data[:functions].each_pair do |key, value|
      if @options['prefix']
        k = key.gsub(@options['prefix'], '')
      else
        k = key
      end
      group, rest = k.split('_', 2)
      next if group.empty?
      if !rest
        group = value[:file].gsub('.h', '').gsub('/', '_')
      end
      @data[:functions][key][:group] = group
      @groups[key] = group
      func[group] ||= []
      func[group] << key
      func[group].sort!
    end
    misc = []
    func.to_a.sort
  end

  def headers(index = nil)
    h = []
    index.each do |entry|
      next unless entry[:path].match(/\.h$/)
      h << entry[:path]
    end
    h
  end

  def find_type_usage
    # go through all the functions and see where types are used and returned
    # store them in the types data
    @data[:functions].each do |func, fdata|
      @data[:types].each_with_index do |tdata, i|
        type, typeData = tdata
        @data[:types][i][1][:used] ||= {:returns => [], :needs => []}
        if fdata[:return][:type].index(/#{type}[ ;\)\*]/)
          @data[:types][i][1][:used][:returns] << func
          @data[:types][i][1][:used][:returns].sort!
        end
        if fdata[:argline].index(/#{type}[ ;\)\*]/)
          @data[:types][i][1][:used][:needs] << func
          @data[:types][i][1][:used][:needs].sort!
        end
      end
    end
  end

  def parse_header(index, path)
    id = index[path][:oid]
    blob = @repo.lookup(id)
    parser = Docurium::CParser.new
    parser.parse_text(path, blob.content)
  end

  def update_globals(recs)
    wanted = {
      :functions => %W/type value file line lineto args argline sig return group description comments/.map(&:to_sym),
      :types => %W/type value file line lineto block tdef comments/.map(&:to_sym),
      :globals => %W/value file line comments/.map(&:to_sym),
      :meta => %W/brief defgroup ingroup comments/.map(&:to_sym),
    }

    file_map = {}

    md = Redcarpet::Markdown.new Redcarpet::Render::HTML, :no_intra_emphasis => true
    recs.each do |r|

      # initialize filemap for this file
      file_map[r[:file]] ||= {
        :file => r[:file], :functions => [], :meta => {}, :lines => 0
      }
      if file_map[r[:file]][:lines] < r[:lineto]
        file_map[r[:file]][:lines] = r[:lineto]
      end

      # process this type of record
      case r[:type]
      when :function
        @data[:functions][r[:name]] ||= {}
        wanted[:functions].each do |k|
          next unless r.has_key? k
          conents = nil
          if k == :description || k == :comments
            contents = md.render r[k]
          else
            contents = r[k]
          end
          @data[:functions][r[:name]][k] = contents
        end
        file_map[r[:file]][:functions] << r[:name]

      when :define, :macro
        @data[:globals][r[:decl]] ||= {}
        wanted[:globals].each do |k|
          next unless r.has_key? k
          if k == :description || k == :comments
            @data[:globals][r[:decl]][k] = md.render r[k]
          else
            @data[:globals][r[:decl]][k] = r[k]
          end
        end

      when :file
        wanted[:meta].each do |k|
          file_map[r[:file]][:meta][k] = r[k] if r.has_key?(k)
        end

      when :enum
        if !r[:name]
          # Explode unnamed enum into multiple global defines
          r[:decl].each do |n|
            @data[:globals][n] ||= {
              :file => r[:file], :line => r[:line],
              :value => "", :comments => md.render(r[:comments]),
            }
            m = /#{Regexp.quote(n)}/.match(r[:body])
            if m
              @data[:globals][n][:line] += m.pre_match.scan("\n").length
              if m.post_match =~ /\s*=\s*([^,\}]+)/
                @data[:globals][n][:value] = $1
              end
            end
          end
        else # enum has name
          @data[:types][r[:name]] ||= {}
          wanted[:types].each do |k|
            next unless r.has_key? k
            if k == :comments
              contents = md.render r[k]
            else
              contents = r[k]
            end
            @data[:types][r[:name]][k] = contents
          end
        end

      when :struct, :fnptr
        @data[:types][r[:name]] ||= {}
        r[:value] ||= r[:name]
        wanted[:types].each do |k|
          next unless r.has_key? k
          if k == :comments
            @data[:types][r[:name]][k] = md.render r[k]
          else
            @data[:types][r[:name]][k] = r[k]
          end
        end
        if r[:type] == :fnptr
          @data[:types][r[:name]][:type] = "function pointer"
        end

      else
        # Anything else we want to record?
      end

    end

    @data[:files] << file_map.values[0]
  end

  def add_dir_to_index(index, prefix, dir)
    Dir.new(dir).each do |filename|
      next if [".", ".."].include? filename
      name = File.join(dir, filename)
      if File.directory? name
        add_dir_to_index(index, prefix, name)
      else
        rel_path = name.gsub(prefix, '')
        content = File.read(name)
        sha = @repo.write(content, :blob)
        index.add(:path => rel_path, :oid => sha, :mode => 0100644)
      end
    end
  end

  def write_site(index)
    here = File.expand_path(File.dirname(__FILE__))
    dirname = File.join(here, '..', 'site')
    dirname = File.realpath(dirname)
    add_dir_to_index(index, dirname + '/', dirname)
  end

  def out(text)
    puts text
  end
end