require 'rexml/document'
require 'prawn'

class Prawn::Svg
  include Prawn::Measurements
  
  attr_reader :data, :prawn, :options
  attr_accessor :scale

  def initialize(data, prawn, options)
    @data = data
    @prawn = prawn
    @options = options
  end

  def draw
    root = parse_document
    calculate_dimensions

    prawn.bounding_box(@options[:at], :width => @width, :height => @height) do
      prawn.save_graphics_state do
        call_tree = generate_call_tree(root)
        proc_creator(prawn, call_tree).call
      end
    end
  end
  
  def generate_call_tree(element)
    [].tap {|calls| parse_element(element, calls, {})}
  end
  
  
  protected  
  def proc_creator(prawn, calls)
    Proc.new {issue_prawn_command(prawn, calls)}
  end
  
  def issue_prawn_command(prawn, calls)
    calls.each do |call, arguments, children|
      if children.empty?
        rewrite_call_arguments(prawn, call, arguments)
        prawn.send(call, *arguments)
      else
        prawn.send(call, *arguments, &proc_creator(prawn, children))
      end
    end
  end
  
  def rewrite_call_arguments(prawn, call, arguments)
    if call == 'text_box'
      if (anchor = arguments.last.delete(:text_anchor)) && %w(middle end).include?(anchor)
        width = prawn.width_of(*arguments)
        width /= 2 if anchor == 'middle'
        arguments.last[:at][0] -= width
      end
      
      arguments.last[:at][1] += prawn.height_of(*arguments) / 3 * 2
    end
  end

  def parse_document
    REXML::Document.new(@data).root.tap do |root|    
      if vb = root.attributes['viewBox']
        x1, y1, x2, y2 = vb.strip.split(/\s+/)
        @x_offset, @y_offset = [x1.to_f, y1.to_f]
        @actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f]
      else
        @x_offset, @y_offset = [0, 0]
        @actual_width = root.attributes['width'].to_f
        @actual_height = root.attributes['height'].to_f
      end
    end
  end
    
  def parse_element(element, calls, state)
    attrs = element.attributes

    if transform = attrs['transform']
      parse_css_method_calls(transform).each do |name, arguments|
        case name
        when 'translate'
          x, y = arguments
          x, y = x.split(/\s+/) if y.nil?
          calls << [name, [distance(x), -distance(y)], []]
          calls = calls.last.last
        when 'rotate'          
          calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []]
          calls = calls.last.last
        when 'scale'
          calls << [name, [arguments.first.to_f], []]
          calls = calls.last.last
        else
          #raise "unknown transformation '#{name}'"
        end
      end
    end

    calls, style_attrs, draw_type = apply_styles(attrs, calls, state)    

    state[:draw_type] = draw_type if draw_type != ""
    if state[:draw_type] && !%w(g svg).include?(element.name)
      calls << [state[:draw_type], [], []]
      calls = calls.last.last
    end
  
    case element.name
    when 'defs', 'desc'
      # ignore these tags
      
    when 'g', 'svg'
      element.elements.each do |child|
        parse_element(child, calls, state.dup)
      end

    when 'text'
      # very primitive support for fonts
      if (font = style_attrs['font-family']) && !font.match(/[\/\\]/)
        font = font.strip
        if font != ""
          calls << ['font', [font], []]
          calls = calls.last.last
        end
      end
      
      opts = {:at => [x(attrs['x']), y(attrs['y'])]}
      if size = style_attrs['font-size']
        opts[:size] = size.to_f * @scale
      end
            
      # This is not a prawn option but we can't work out how to render it here - it's handled by #rewrite_call
      if anchor = style_attrs['text-anchor']
        opts[:text_anchor] = anchor        
      end
      
      calls << ['text_box', [element.text, opts], []]

    when 'line'
      calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []]

    when 'polyline'
      points = attrs['points'].split(/\s+/)
      x, y = points.shift.split(",")
      calls << ['move_to', [x(x), y(y)], []]
      calls << ['stroke', [], []]
      calls = calls.last.last
      points.each do |point|
        x, y = point.split(",")
        calls << ["line_to", [x(x), y(y)], []]
      end
    
    when 'polygon'
      points = attrs['points'].split(/\s+/).collect do |point|
        x, y = point.split(",")
        [x(x), y(y)]
      end
      calls << ["polygon", points, []]      
      
    when 'circle'
      calls << ["circle_at", 
        [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}], 
        []]
      
    when 'ellipse'
      calls << ["ellipse_at", 
        [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])],
        []]
      
    when 'rect'
      radius = distance(attrs['rx'] || attrs['ry'])
      args = [[x(attrs['x']), y(attrs['y'])], distance(attrs['width']), distance(attrs['height'])]
      if radius
        # n.b. does not support both rx and ry being specified with different values
        calls << ["rounded_rectangle", args + [radius], []]
      else
        calls << ["rectangle", args, []]
      end
      
    when 'path'
      @svg_path ||= Path.new
      @svg_path.parse(attrs['d']).each do |command, args|
        point_to = [x(args[0]), y(args[1])]
        if command == 'curve_to'
          bounds = [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]
          calls << [command, [point_to, {:bounds => bounds}], []]
        else
          calls << [command, point_to, []]
        end
      end

    else 
      #raise "unknown tag #{element.name}"
    end
  end
  
  def parse_css_declarations(declarations)
    # copied from css_parser
    declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')

    {}.tap do |o|
      declarations.split(/[\;$]+/m).each do |decs|
        if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
          property, value, end_of_declaration = matches.captures
          o[property] = value
        end
      end
    end
  end
  
  def apply_styles(attrs, calls, state)
    draw_types = []

    decs = attrs["style"] ? parse_css_declarations(attrs["style"]) : {}
    attrs.each {|n,v| decs[n] = v unless decs[n]}
            
    # Opacity:
    # We can't do nested opacities quite like the SVG requires, but this is close enough.
    fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity']
    fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity']
    stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity']
    
    if fill_opacity || stroke_opacity      
      state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1)
      state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1)

      calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []] 
      calls = calls.last.last
    end

    if decs['fill'] && decs['fill'] != "none"
      if color = color_to_hex(decs['fill'])
        calls << ['fill_color', [color], []]
      end
      draw_types << 'fill'
    end
    
    if decs['stroke'] && decs['stroke'] != "none"
      if color = color_to_hex(decs['stroke'])
        calls << ['stroke_color', [color], []]
      end
      draw_types << 'stroke'
    end
    
    calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width']          
        
    [calls, decs, draw_types.join("_and_")]
  end
  
  def parse_css_method_calls(string)
    string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
      name, argument_string = call
      arguments = argument_string.split(",").collect(&:strip)
      [name, arguments]
    end    
  end

  # TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords
  HTML_COLORS = {    
  	'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00",
  	'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00",
  	'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
  	'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
  }.freeze
  
  def color_to_hex(color_string)
    color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_|
      if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
        break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}"
      elsif color.match(/\A#[0-9a-f]{6}\z/i)
        break color[1..6]
      elsif hex = HTML_COLORS[color.downcase]
        break hex
      elsif m = color.match(/\Argb\(\s*(-?[0-9.]+%?)\s*,\s*(-?[0-9.]+%?)\s*,\s*(-?[0-9.]+%?)\s*\)\z/i)
        break (1..3).collect do |n|
          value = m[n].to_f
          value *= 2.55 if m[n][-1..-1] == '%'
          "%02x" % clamp(value.round, 0, 255)
        end.join        
      end    
    end
  end
  
  def x(value)
    (pixels(value) - @x_offset) * scale
  end
  
  def y(value)
    (@actual_height - (pixels(value) - @y_offset)) * scale
  end
  
  def distance(value)
    value && (pixels(value) * scale)
  end
  
  def pixels(value)
    if value.is_a?(String) && match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
      send("#{match[1]}2pt", value.to_f)
    else
      value.to_f
    end
  end
  
  def calculate_dimensions    
    if @options[:width]
      @width = @options[:width]      
      @scale = @options[:width] / @actual_width.to_f
    elsif @options[:height]
      @height = @options[:height]
      @scale = @options[:height] / @actual_height.to_f
    else
      @scale = 1
    end
    
    @width ||= @actual_width * @scale
    @height ||= @actual_height * @scale
  end
  
  def clamp(value, min_value, max_value)
    [[value, min_value].max, max_value].min
  end  
end