$:.unshift File.dirname(__FILE__), 'lib'
require 'html5'
require 'ostruct'
require 'optparse'

module HTML5::CLI

  def self.parse_opts argv
    options = OpenStruct.new
    options.profile        = false
    options.time           = false
    options.output         = :html
    options.treebuilder    = 'simpletree'
    options.error          = false
    options.encoding       = false
    options.parsemethod    = :parse
    options.serializer     = {
      :encoding            => 'utf-8',
      :omit_optional_tags  => false,
      :inject_meta_charset => false
    }

    opts = OptionParser.new do |opts|
      opts.separator ""
      opts.separator "Parse Options:"

      opts.on("-b", "--treebuilder NAME") do |treebuilder|
        options.treebuilder = treebuilder
      end

      opts.on("-f", "--fragment CONTAINER", "Parse as a fragment") do |container|
        options.parsemethod = :parse_fragment
        options.container = container if container
      end

      opts.separator ""
      opts.separator "Filter Options:"

      opts.on("--[no-]inject-meta-charset", "inject <meta charset>") do |inject|
        options.serializer[:inject_meta_charset] = inject
      end

      opts.on("--[no-]strip-whitespace", "strip unnecessary whitespace") do |strip|
        options.serializer[:strip_whitespace] = strip
      end

      opts.on("--[no-]sanitize", "escape unsafe tags") do |sanitize|
        options.serializer[:sanitize] = sanitize
      end

      opts.separator ""
      opts.separator "Output Options:"

      opts.on("--tree", "output as debug tree") do |tree|
        options.output = :tree
      end

      opts.on("-x", "--xml", "output as xml") do |xml|
        options.output = :xml
        options.treebuilder = "rexml"
      end

      opts.on("--[no-]html", "Output as html") do |html|
        options.output = (html ? :html : nil)
      end

      opts.on("--hilite", "Output as formatted highlighted code.") do |hilite|
        options.output = :hilite
      end

      opts.on("-e", "--error", "Print a list of parse errors") do |error|
        options.error = error
      end

      opts.separator ""
      opts.separator "Serialization Options:"

      opts.on("--[no-]omit-optional-tags", "Omit optional tags") do |omit|
        options.serializer[:omit_optional_tags] = omit
      end

      opts.on("--[no-]quote-attr-values", "Quote attribute values") do |quote|
        options.serializer[:quote_attr_values] = quote
      end

      opts.on("--[no-]use-best-quote-char", "Use best quote character") do |best|
        options.serializer[:use_best_quote_char] = best
      end

      opts.on("--quote-char C", "Use specified quote character") do |c|
        options.serializer[:quote_char] = c
      end

      opts.on("--[no-]minimize-boolean-attributes", "Minimize boolean attributes") do |min|
        options.serializer[:minimize_boolean_attributes] = min
      end

      opts.on("--[no-]use-trailing-solidus", "Use trailing solidus") do |slash|
        options.serializer[:use_trailing_solidus] = slash
      end

      opts.on("--[no-]escape-lt-in-attrs", "Escape less than signs in attribute values") do |lt|
        options.serializer[:escape_lt_in_attrs] = lt
      end

      opts.on("--[no-]escape-rcdata", "Escape rcdata element values") do |rcdata|
        options.serializer[:escape_rcdata] = rcdata
      end

      opts.separator ""
      opts.separator "Other Options:"

      opts.on("-p", "--[no-]profile", "Profile the run") do |profile|
        options.profile = profile
      end

      opts.on("-t", "--[no-]time", "Time the run") do |time|
        options.time = time
      end

      opts.on("-c", "--[no-]encoding", "Print character encoding used") do |encoding|
        options.encoding = encoding
      end

      opts.on_tail("-h", "--help", "Show this message") do
        puts opts
        exit
      end


    end
    opts.parse!(argv)
    options
  end

  def self.open_input f
    if f
      begin
        if f[0..6] == 'http://'
          require 'open-uri'
          f = URI.parse(f).open
          encoding = f.charset
        elsif f == '-'
          f = $stdin
        else
          f = open(f)
        end
      rescue
      end
    else
      $stderr.write("No filename provided. Use -h for help\n")
      exit(1)
    end
    f
  end

  def self.parse(opts, args)
    encoding = nil

    f = open_input args.last

    require 'html5/treebuilders'
    treebuilder = HTML5::TreeBuilders[opts.treebuilder]

    if opts.output == :xml
      require 'html5/liberalxmlparser'
      p = HTML5::XMLParser.new(:tree=>treebuilder)
    else
      require 'html5/html5parser'
      p = HTML5::HTMLParser.new(:tree=>treebuilder)
    end

    if opts.parsemethod == :parse
      args = [f, encoding]
    else
      args = [f, (opts.container || 'div'), encoding]
    end

    if opts.profile
      require 'profiler'
      Profiler__::start_profile
      p.send(opts.parsemethod, *args)
      Profiler__::stop_profile
      Profiler__::print_profile($stderr)
    elsif opts.time
      require 'time' # TODO: switch to benchmark
      t0 = Time.new
      document = p.send(opts.parsemethod, *args)
      t1 = Time.new
      print_output(p, document, opts)
      t2 = Time.new
      puts "\n\nRun took: #{t1-t0}s (plus #{t2-t1}s to print the output)"
    else
      document = p.send(opts.parsemethod, *args)
      print_output(p, document, opts)
    end
  end

  def self.print_output(parser, document, opts)
    puts "Encoding: #{parser.tokenizer.stream.char_encoding}" if opts.encoding

    case opts.output
    when :xml
      print document
    when :html
      require 'html5/treewalkers'
      tokens = HTML5::TreeWalkers[opts.treebuilder].new(document)
      require 'html5/serializer'
      puts HTML5::HTMLSerializer.serialize(tokens, opts.serializer)
    when :hilite
      print document.hilite
    when :tree
      document = [document] unless document.respond_to?(:each)
      document.each {|fragment| puts parser.tree.testSerializer(fragment)}
    end

    if opts.error
      errList=[]
      for pos, errorcode, datavars in parser.errors
        formatstring = HTML5::E[errorcode] || 'Unknown error "%(errorcode)"'
        message = PythonicTemplate.new(formatstring).to_s(datavars)
        errList << "Line #{pos[0]} Col #{pos[1]} " + message
      end
      $stdout.write("\nParse errors:\n" + errList.join("\n")+"\n")
    end
  end

  class PythonicTemplate
    # convert Python format string into a Ruby string, ready to eval
    def initialize format
      @format = format
      @format.gsub!('"', '\\"')
      @format.gsub!(/%\((\w+)\)/, '#{@_\1}')
      @format = '"' + @format + '"'
    end

    # evaluate string
    def to_s(vars=nil)
      vars.each {|var,value| eval "@_#{var}=#{value.dump}"} if vars
      eval @format
    end
  end

  def self.run
    options = parse_opts ARGV
    parse options, ARGV
  end
end