require 'rexml/text'
require 'pathname'
require 'erb'
#require 'reap/project/scm'

module DNote

  # = Developer Notes
  #
  # This class goes through you source files and compiles
  # an list of any labeled comments. Labels are single word
  # prefixes to a comment ending in a colon.
  #
  # By default the labels supported are TODO, FIXME, OPTIMIZE and DEPRECATE.
  #
  # Output is a set of files in XML and RDoc's simple
  # markup format.
  #
  #   TODO: Add ability to read header notes. They oftern
  #         have a outline format, rather then the single line.
  #
  #   TODO: Need good CSS file.
  #
  #   TODO: Need XSL?
  #
  class Notes

    # Default note labels to look for in source code.
    DEFAULT_LABELS = ['TODO', 'FIXME', 'OPTIMIZE', 'DEPRECATE']

    #
    DEFAULT_OUTPUT = Pathname.new('log/notes')

    #
    attr_accessor :title

    # Non-Operative
    attr_accessor :noop

    # Verbose
    attr_accessor :verbose

    # Paths to search.
    attr_accessor :paths

    # Labels to document. Defaults are: TODO, FIXME, OPTIMIZE and DEPRECATE.
    attr_accessor :labels

    # Directory to save output. Defaults to standard log directory.
    attr_accessor :output

    # Format (xml, html, text).
    # TODO: HTML format is not usable yet.
    #attr_accessor :format

    #
    def initialize(paths, options={})
      initialize_defaults

      if paths.empty?
        if file = File.exist?('meta/loadpath')
          paths = YAML.load(File.new(file)).to_list
          paths = Array === paths ? paths : paths.split(/\s+/)
        elsif file = File.exist?('lib')
          paths = ['lib']
        else
          paths = ['**/*.rb']
        end
      end

      @paths = paths

      options.each do |k, v|
        __send__("#{k}=", v)
      end
    end

    #
    def initialize_defaults
      @paths    = ['lib']
      @output   = DEFAULT_OUTPUT
      @labels   = DEFAULT_LABELS
      @title    = "Developer's Notes"
      #@format   = 'xml'
    end

    #
    def noop?
      @noop
    end

    #
    def verbose?
      @verbose
    end

    #
    def notes
      @notes
    end

    #
    def counts
      @counts
    end

    #
    def templates
      Dir[File.join(File.dirname(__FILE__), 'template/*')]
    end

    # Scans source code for developer notes and writes them to 
    # well organized files.
    #
    def document
      paths    = self.paths
      output   = self.output

      parse

      #paths  = paths.to_list

      #labels = labels.split(',') if String === labels
      #labels = [labels].flatten.compact

      #records, counts = extract(labels, loadpath)
      #records = organize(records)

      #case format.to_s
      #when 'rdoc', 'txt', 'text'
      #  text = format_rd(records)
      #else
      #  text = format_xml(records)
      #end

      if notes.empty?
        puts "No #{labels.join(', ')} notes."
      else
        templates.each do |template|
          erb  = ERB.new(File.read(template))
          text = erb.result(binding)
          #text = format_notes(notes, format)
          file = write(File.basename(template), text)
          #file = file #Pathname.new(file).relative_path_from(Pathname.pwd) #project.root
          puts "Updated #{file}"
        end
        puts(counts.map{|l,n| "#{n} #{l}s"}.join(', '))
      end
    end

    # Reset output directory, marking it as out-of-date.
    def reset
      if File.directory?(output)
        File.utime(0,0,output)
        puts "marked #{output}"
      end
    end

    # Remove output directory.
    def clean
      if File.directory?(output)
        fu.rm_r(output)
        puts "removed #{output}"
      end
    end

    #
    def labels=(labels)
      @labels = (
        case labels
        when String
          labels.split(/[:;,]/)
        else
          labels = [labels].flatten.compact.uniq
        end
      )
    end

    #
    def output=(output)
      raise "output cannot be root" if File.expand_path(output) == "/"
      @output = Pathname.new(output)
    end

  private

    # Gather and count notes. This returns two elements,
    # a hash in the form of label=>notes and a counts hash.
    #
    def parse
      #
      files = self.paths.map do |path|
        if File.directory?(path)
          Dir.glob(File.join(path, '**/*'))
        else
          Dir.glob(path)
        end
      end.flatten.uniq

      #
      records, counts = [], Hash.new(0)

      # iterate through files extracting notes
      files.each do |fname|
        next unless File.file?(fname)
        #next unless fname =~ /\.rb$/      # TODO should this be done?
        File.open(fname) do |f|
          line_no, save, text = 0, nil, nil
          while line = f.gets
            line_no += 1
            labels.each do |label|
              if line =~ /^\s*#\s*#{Regexp.escape(label)}[:]?\s*(.*?)$/
                file = fname
                text = ''
                save = {'label'=>label,'file'=>file,'line'=>line_no,'note'=>text}
                records << save
                counts[label] += 1
              end
            end
            if text
              if line =~ /^\s*[#]{0,1}\s*$/ or line !~ /^\s*#/ or line =~ /^\s*#[+][+]/
                text.strip!
                text = nil
                #records << save
              else
                text << line.gsub(/^\s*#\s*/,'')
              end
            end
          end
        end
      end
      # organize the notes
      notes = organize(records)
      #
      @notes, @counts = notes, counts
    end

    # Organize records in heirarchical form.
    #
    def organize(records)
      orecs = {}
      records.each do |record|
        label = record['label']
        file  = record['file']
        line  = record['line']
        note  = record['note'].rstrip
        orecs[label] ||= {}
        orecs[label][file] ||= []
        orecs[label][file] << [line, note]
      end
      orecs
    end

    #
    #def format_notes(notes, type=:rdoc)
    #  send("format_#{type}", notes)
    #end

    # Format notes in XML format.
    #
    def notes_xml
      xml = []
      xml << "<notes>"
      notes.each do |label, per_file|
        xml << %[<set label="#{label}">]
        per_file.each do |file, line_notes|
          xml << %[<file src="#{file}">]
          line_notes.sort!{ |a,b| a[0] <=> b[0] }
          line_notes.each do |line, note|
            note = REXML::Text.normalize(note)
            xml << %[<note line="#{line}" type="#{label}">#{note}</note>]
          end
          xml << %[</file>]
        end
        xml << %[</set>]
      end
      xml << "</notes>"
      return xml.join("\n")
    end

    # Format notes in RDoc format.
    #
    def notes_rdoc
      out = []
      out << "= Development Notes"
      notes.each do |label, per_file|
        out << %[\n== #{label}]
        per_file.each do |file, line_notes|
          out << %[\n=== file://#{file}]
          line_notes.sort!{ |a,b| a[0] <=> b[0] }
          line_notes.each do |line, note|
            out << %[* #{note} (#{line})]
          end
        end
      end
      return out.join("\n")
    end

    # HTML format.
    #
    def notes_html
      xml = []
      xml << %[<div class="notes">]
      notes.each do |label, per_file|
        xml << %[<h2>#{label}</h2>\n<ol class="set #{label.downcase}">]
        per_file.each do |file, line_notes|
          xml << %[<li><h3><a href="#{file}">#{file}</a></h3><ol class="file" href="#{file}">]
          line_notes.sort!{ |a,b| a[0] <=> b[0] }
          line_notes.each do |line, note|
            note = REXML::Text.normalize(note)
            xml << %[<li class="note #{label.downcase}" ref="#{line}">#{note} <sup>#{line}</sup></li>]
          end
          xml << %[</ol></li>]
        end
        xml << %[</div>]
      end
      xml << "</div>"
      return xml.join("\n")
    end

    # Save notes.
    #
    def write(file, text)
      file = output + file
      fu.mkdir_p(output)
      File.open(file, 'w') { |f| f << text } unless noop?
      file
    end

    #
    def fu
      @fu ||= (
        if noop? and verbose?
          FileUtils::DryRun
        elsif noop
          FileUtils::Noop
        elsif verbose
          FileUtils::Verbose
        else
          FileUtils
        end
      )
    end

  end

end

  #     out = ''
  #
  #     case format
  #     when 'yaml'
  #       out << records.to_yaml
  #     when 'list'
  #       records.each do |record|
  #         out << "* #{record['note']}\n"
  #       end
  #     else #when 'rdoc'
  #       labels.each do |label|
  #         recs = records.select{ |r| r['label'] == label }
  #         next if recs.empty?
  #         out << "\n= #{label}\n"
  #         last_file = nil
  #         recs.sort!{ |a,b| a['file'] <=> b['file'] }
  #         recs.each do |record|
  #           if last_file != record['file']
  #             out << "\n"
  #             last_file = record['file']
  #             out << "file://#{record['file']}\n"
  #           end
  #           out << "* #{record['note'].rstrip} (#{record['line']})\n"
  #         end
  #       end
  #       out << "\n---\n"
  #       out << counts.collect{|l,n| "#{n} #{l}s"}.join(' ')
  #       out << "\n"
  #     end

  #     # List TODO notes. Same as notes --label=TODO.
  #
  #     def todo( options={} )
  #       options = options.to_openhash
  #       options.label = 'TODO'
  #       notes(options)
  #     end
  #
  #     # List FIXME notes.  Same as notes --label=FIXME.
  #
  #     def fixme( options={} )
  #       options = options.to_openhash
  #       options.label = 'FIXME'
  #       notes(options)
  #     end