require 'stringio'

module RRTF
  # This class represents properties that are to be applied to geometry
  # objects.
  # @author Wesley Hileman
  # @since 1.0.0
  class GeometryProperties < Properties

    HORIZONTAL_REFERENCE_DICTIONARY = {
      "MARGIN"          => 0,
      "PAGE"            => 1,
      "COLUMN"          => 2,
      "CHARACTER"       => 3,
      "LEFT_MARGIN"     => 4,
      "RIGHT_MARGIN"    => 5,
      "INSIDE_MARGIN"   => 6,
      "OUTSIDE_MARGIN"  => 7
    }.freeze

    VERTICAL_REFERENCE_DICTIONARY = {
      "MARGIN"          => 0,
      "PAGE"            => 1,
      "PARAGRAPH"       => 2,
      "LINE"            => 3,
      "TOP_MARGIN"      => 4,
      "BOTTOM_MARGIN"   => 5,
      "INSIDE_MARGIN"   => 6,
      "OUTSIDE_MARGIN"  => 7
    }.freeze

    HORIZONTAL_ALIGNMENT_DICTIONARY = {
      "ABSOLUTE"        => 0,
      "LEFT"            => 1,
      "CENTER"          => 2,
      "RIGHT"           => 3,
      "INSIDE"          => 4,
      "OUTSIDE"         => 5
    }.freeze

    VERTICAL_ALIGNMENT_DICTIONARY = {
      "ABSOLUTE"        => 0,
      "TOP"             => 1,
      "CENTER"          => 2,
      "BOTTOM"          => 3,
      "INSIDE"          => 4,
      "OUTSIDE"         => 5
    }.freeze

    WIDTH_REFERENCE_DICTIONARY = {
      "MARGIN"          => 0,
      "PAGE"            => 1,
      "LEFT_MARGIN"     => 2,
      "RIGHT_MARGIN"    => 3,
      "INSIDE_MARGIN"   => 4,
      "OUTSIDE_MARGIN"  => 5
    }.freeze

    HEIGHT_REFERENCE_DICTIONARY = {
      "MARGIN"          => 0,
      "PAGE"            => 1,
      "TOP_MARGIN"      => 2,
      "BOTTOM_MARGIN"   => 3,
      "INSIDE_MARGIN"   => 4,
      "OUTSIDE_MARGIN"  => 5
    }.freeze

    TEXT_WRAP_DICTIONARY = {
      "INLINE"                => {"WRAP" => 1, "SIDE" => nil},
      "AROUND_BOTH"           => {"WRAP" => 2, "SIDE" => 0},
      "AROUND_LEFT"           => {"WRAP" => 2, "SIDE" => 1},
      "AROUND_RIGHT"          => {"WRAP" => 2, "SIDE" => 2},
      "AROUND_LARGEST"        => {"WRAP" => 2, "SIDE" => 3},
      "NONE"                  => {"WRAP" => 3, "SIDE" => nil},
      "TIGHT_AROUND_BOTH"     => {"WRAP" => 4, "SIDE" => 0},
      "TIGHT_AROUND_LEFT"     => {"WRAP" => 4, "SIDE" => 1},
      "TIGHT_AROUND_RIGHT"    => {"WRAP" => 4, "SIDE" => 2},
      "TIGHT_AROUND_LARGEST"  => {"WRAP" => 4, "SIDE" => 3},
    }.freeze

    GEOMERTY_TYPE_DICTIONARY = {
      "CUSTOM"                => 0,
      "RECTANGLE"             => 1,
      "ROUND_RECTANGLE"       => 2,
      "ELLIPSE"               => 3,
      "DIAMOND"               => 4,
      "ISOSCELES_TRIANGLE"    => 5,
      "RIGHT_TRIANGLE"        => 6,
      "PARALLELOGRAM"         => 7,
      "TRAPEZOID"             => 8,
      "HEXAGON"               => 9,
      "OCTAGON"               => 10,
      "PENTAGON"              => 56,
      "LINE"                  => 20,
      "TEXT_BOX"              => 202
    }.freeze

    TEXT_ANCHOR_DICTIONARY = {
      "TOP"                       => 0,
      "MIDDLE"                    => 1,
      "BOTTOM"                    => 2,
      "TOP_CENTERED"              => 3,
      "MIDDLE_CENTERED"           => 4,
      "BOTTOM_CENTERED"           => 5,
      "TOP_BASELINE"              => 6,
      "BOTTOM_BASELINE"           => 7,
      "TOP_CENTERED_BASELINE"     => 8,
      "BOTTOM_CENTERED_BASELINE"  => 9
    }.freeze

    # @note The upper three bits store segment stype, lower 13 bits store the
    #   number of segments of that type to appear in series (always 1 -- except
    #   for control segments -- for the non-compressed encoding used here
    #   where the codes for segments of the same type that appear in series are
    #   repeated).
    PATH_SEGMENT_DICTIONARY = {
      # draw a line from the current point to a specified end point
      # [requires one additional point]
      "LINE_TO"                   => "0001".to_i(16),
      # draw a cubic bezier curve using the current point, two control points,
      # and an end point [requires three additional points]
      "CUBIC_BEZIER_TO"           => "2001".to_i(16),
      # draw a line from the current point to the starting point and close
      # the path [requires no additional points]
      "CLOSE_PATH"                => "6001".to_i(16),
      # start a path (control segment) [requires one point]
      "START_AT"                  => "4000".to_i(16),
      # end a path (control segment) [requires no points]
      "END"                       => "8000".to_i(16)
    }.freeze

     # This is a constructor for the GeometryProperties class.
     #
     # @param [Hash] options
     def initialize(options = {})
       @type                        = GEOMERTY_TYPE_DICTIONARY[options.delete("type")]
       @rotation                    = Utilities.value2geomfrac(options.delete("rotate"))
       @left                        = Utilities.value2twips(options.delete("left"))
       @right                       = Utilities.value2twips(options.delete("right"))
       @top                         = Utilities.value2twips(options.delete("top"))
       @bottom                      = Utilities.value2twips(options.delete("bottom"))
       @z_index                     = options.delete("z_index")
       @horizontal_reference        = HORIZONTAL_REFERENCE_DICTIONARY[options.delete("horizontal_reference")] || HORIZONTAL_REFERENCE_DICTIONARY["MARGIN"]
       @vertical_reference          = VERTICAL_REFERENCE_DICTIONARY[options.delete("vertical_reference")] || VERTICAL_REFERENCE_DICTIONARY["MARGIN"]
       @text_wrap                   = TEXT_WRAP_DICTIONARY[options.delete("text_wrap")]
       @below_text                  = Utilities.value2geombool(options.delete("below_text"))
       @lock_anchor                 = options.delete("lock_anchor")
       @horizontal_alignment        = HORIZONTAL_ALIGNMENT_DICTIONARY[options.delete("horizontal_alignment")] || HORIZONTAL_ALIGNMENT_DICTIONARY["ABSOLUTE"]
       @vertical_alignment          = VERTICAL_ALIGNMENT_DICTIONARY[options.delete("vertical_alignment")] || VERTICAL_ALIGNMENT_DICTIONARY["ABSOLUTE"]
       @allow_overlap               = Utilities.value2geombool(options.delete("allow_overlap"))
       @width_reference             = WIDTH_REFERENCE_DICTIONARY[options.delete("width_reference")] || WIDTH_REFERENCE_DICTIONARY["MARGIN"]
       @height_reference            = HEIGHT_REFERENCE_DICTIONARY[options.delete("height_reference")] || HEIGHT_REFERENCE_DICTIONARY["MARGIN"]
       @width, @width_units         = Utilities.parse_string_with_units(options.delete("width"))
       @height, @height_units       = Utilities.parse_string_with_units(options.delete("height"))
       @width                       = Utilities.value2twips("#{@width}#{@width_units}") unless @width.nil? || @width_units == '%'
       @height                      = Utilities.value2twips("#{@height}#{@height_units}") unless @height.nil? || @height_units == '%'
       @fill_color                  = options.delete("fill_color")
       @has_fill                    = Utilities.value2geombool(options.delete("has_fill") || !@fill_color.nil?)
       @line_color                  = options.delete("line_color")
       @line_width                  = Utilities.value2emu(options.delete("line_width"))
       @has_line                    = Utilities.value2geombool(options.delete("has_line") || !@line_color.nil? || !@line_width.nil?)
       @text_margin                 = options.delete("text_margin")
       @text_anchor                 = TEXT_ANCHOR_DICTIONARY[options.delete("text_anchor")]
       @fit_to_text                 = Utilities.value2geombool(options.delete("fit_to_text"))
       @fit_text_to_shape           = Utilities.value2geombool(options.delete("fit_text_to_shape"))
       @flip_horizontal             = Utilities.value2geombool(options.delete("flip_horizontal"))
       @flip_vertical               = Utilities.value2geombool(options.delete("flip_vertical"))
       @path                        = options.delete("path")
       @path_coordinate_origin      = options.delete("path_coordinate_origin") || [0, 0]
       @path_coordinate_limits      = options.delete("path_coordinate_limits") || [21600, 21600]

       parse_dimensions!
       parse_color! :fill_color, :line_color
       parse_margin! :text_margin
       parse_path!

       unless options.empty?
         RTFError.fire("Unreconized geometry options #{options}.")
       end # unless
     end # initialize()

     # Converts a geometry properties object into an RTF sequence.
     #
     # @return [String] the RTF sequence corresponding to the properties object.
     def to_rtf
       rtf = StringIO.new

       # keyword properties
       rtf << "\\shpleft#{@left}"                         unless @left.nil?
       rtf << "\\shpright#{@right}"                       unless @right.nil?
       rtf << "\\shptop#{@top}"                           unless @top.nil?
       rtf << "\\shpbottom#{@bottom}"                     unless @bottom.nil?
       rtf << "\\shpz#{@z_index}"                         unless @z_index.nil?
       rtf << "\\shpbxpage"                               if @horizontal_reference == HORIZONTAL_REFERENCE_DICTIONARY["PAGE"]
       rtf << "\\shpbxmargin"                             if @horizontal_reference == HORIZONTAL_REFERENCE_DICTIONARY["MARGIN"]
       rtf << "\\shpbxcolumn"                             if @horizontal_reference == HORIZONTAL_REFERENCE_DICTIONARY["COLUMN"]
       rtf << "\\shpbxignore"                             unless @vertical_reference.nil?
       rtf << "\\shpbypage"                               if @vertical_reference == VERTICAL_REFERENCE_DICTIONARY["PAGE"]
       rtf << "\\shpbymargin"                             if @vertical_reference == VERTICAL_REFERENCE_DICTIONARY["MARGIN"]
       rtf << "\\shpbypara"                               if @vertical_reference == VERTICAL_REFERENCE_DICTIONARY["PARAGRAPH"]
       rtf << "\\shpbyignore"                             unless @vertical_reference.nil?
       rtf << "\\shpwr#{@text_wrap["WRAP"]}"              unless @text_wrap.nil? || @text_wrap["WRAP"].nil?
       rtf << "\\shpwrk#{@text_wrap["SIDE"]}"             unless @text_wrap.nil? || @text_wrap["SIDE"].nil?
       rtf << "\\shpfblwtxt#{@below_text}"                unless @below_text.nil?
       rtf << "\\shplockanchor"                           if @lock_anchor

       rtf << "\n"

       # object properties
       rtf << build_property("shapeType", @type)                          unless @type.nil?
       rtf << build_property("rotation", @rotation)                       unless @rotation.nil?
       rtf << build_property("posh", @horizontal_alignment)               unless @horizontal_alignment.nil?
       rtf << build_property("posrelh", @horizontal_reference)            unless @horizontal_reference.nil?
       rtf << build_property("posv", @vertical_alignment)                 unless @vertical_alignment.nil?
       rtf << build_property("posrelv", @vertical_reference)              unless @vertical_reference.nil?
       rtf << build_property("fAllowOverlap", @allow_overlap)             unless @allow_overlap.nil?
       rtf << build_property("pctHoriz", @width)                          unless @width.nil? || @width_units != '%'
       rtf << build_property("pctVert", @height)                          unless @height.nil? || @height_units != '%'
       rtf << build_property("sizerelh", @width_reference)                unless @width_reference.nil?
       rtf << build_property("sizerelv", @height_reference)               unless @height_reference.nil?
       rtf << build_property("fFilled", @has_fill)                        unless @has_fill.nil?
       rtf << build_property("fillColor", @fill_color)                    unless @fill_color.nil?
       rtf << build_property("fLine", @has_line)                          unless @has_fill.nil?
       rtf << build_property("lineColor", @line_color)                    unless @line_color.nil?
       rtf << build_property("lineWidth", @line_width)                    unless @line_width.nil?
       rtf << build_property("dxTextLeft", @text_margin_left)             unless @text_margin_left.nil?
       rtf << build_property("dxTextRight", @text_margin_right)           unless @text_margin_right.nil?
       rtf << build_property("dyTextTop", @text_margin_top)               unless @text_margin_top.nil?
       rtf << build_property("dyTextBottom", @text_margin_bottom)         unless @text_margin_bottom.nil?
       rtf << build_property("anchorText", @text_anchor)                  unless @text_anchor.nil?
       rtf << build_property("fBehindDocument", @below_text)              unless @below_text.nil?
       rtf << build_property("fFitShapeToText", @fit_to_text)             unless @fit_to_text.nil?
       rtf << build_property("fFitTextToShape", @fit_text_to_shape)       unless @fit_text_to_shape.nil?
       rtf << build_property("fFlipH", @flip_horizontal)                  unless @flip_horizontal.nil?
       rtf << build_property("fFlipV", @flip_vertical)                    unless @flip_vertical.nil?
       rtf << build_property("geoLeft", @path_coordinate_origin[0])       unless @path.nil? || @path_coordinate_origin.nil?
       rtf << build_property("geoTop", @path_coordinate_origin[1])        unless @path.nil? || @path_coordinate_origin.nil?
       rtf << build_property("geoRight", @path_coordinate_limits[0])      unless @path.nil? || @path_coordinate_limits.nil?
       rtf << build_property("geoBottom", @path_coordinate_limits[1])     unless @path.nil? || @path_coordinate_limits.nil?
       rtf << build_property("pVerticies", @path_verticies)               unless @path.nil? || @path_verticies.nil?
       rtf << build_property("pSegmentInfo", @path_segment_info)          unless @path.nil? || @path_segment_info.nil?
       rtf << build_property("pConnectionSites", @path_connection_sites)  unless @path.nil? || @path_connection_sites.nil?
       rtf << build_property("fLineOK", 1)
       rtf << build_property("fFillOK", 1)
       rtf << build_property("f3DOK", 1)

       rtf.string
     end

     private

     def build_property(name, value)
       "{\\sp{\\sn #{name}}{\\sv #{value}}}\n"
     end

     def build_array(array, bytes_per_element)
       "#{bytes_per_element};#{array.length};#{array.join(';')}"
     end

     def array2emu(array)
       array.collect{ |el| Utilities.value2emu(el) }
     end

     def parse_dimensions!
       unless @width.nil?
         if @width_units == '%'
           @percent_width = @width
         else
           case [@left.nil?, @right.nil?]
           when [true, true]
             @left = 0
             @right = @width
           when [true, false]
             @left = @right - @width
           when [false, true]
             @right = @left + @width
           end # case
         end # if
       end # unless

       unless @height.nil?
         if @height_units == '%'
           @percent_height = @height
         else
           case [@top.nil?, @bottom.nil?]
           when [true, true]
             @top = 0
             @bottom = @height
           when [true, false]
             @top = @bottom - @height
           when [false, true]
             @bottom = @top + @height
           end # case
         end # if
       end # unless
     end # parse_dimensions()

     def parse_color!(*color_attrs)
       color_attrs.each do |color_attr|
         color = instance_variable_get(:"@#{color_attr}")

         unless color.nil?
           case color
           when String
             color = Colour.from_string(color).to_decimal("reverse_bytes" => true)
           when Colour
             color = color.to_decimal
           else
             RTFError.fire("Unsupported color format #{color}.")
           end # case
         end # unless

         instance_variable_set(:"@#{color_attr}", color)
       end # each
     end # parse_color()

     def parse_margin!(*margin_attrs)
       margin_attrs.each do |margin_attr|
         margin = instance_variable_get("@#{margin_attr}")
         next if margin.nil?

         margin = Page::Margin.new(margin)
         left = Utilities.value2emu(margin.left+'twip')
         right = Utilities.value2emu(margin.right+'twip')
         top = Utilities.value2emu(margin.top+'twip')
         bottom = Utilities.value2emu(margin.bottom+'twip')

         instance_variable_set("@#{margin_attr}", margin)
         instance_variable_set("@#{margin_attr}_left", left)
         instance_variable_set("@#{margin_attr}_right", right)
         instance_variable_set("@#{margin_attr}_top", top)
         instance_variable_set("@#{margin_attr}_bottom", bottom)
       end
     end # parse_margin()

     def parse_path!
       return if @path.nil?

       verticies = []
       connection_sites = []
       seg_info = []

       unless @path.is_a?(Array) && @path.collect{ |tup| tup.is_a?(Array) && (1..5).include?(tup.length) }.all?
         RTFError.fire("Path segments must be an array of arrays with length 1 through 5.")
       end # unless

       sx = (@path_coordinate_limits[0] - @path_coordinate_origin[0]).to_f/(Utilities.value2emu("#{@width}twip")).to_f
       sy = (@path_coordinate_limits[1] - @path_coordinate_origin[1]).to_f/(Utilities.value2emu("#{@height}twip")).to_f

       @path.each do |seg|
         # first item in segment array gives the segment type
         type = seg[0]

         if PATH_SEGMENT_DICTIONARY[type].nil?
           RTFError.fire("Invalid segment type '#{type}'.")
         end # case

         if seg.length > 1
           # remaining items give the points associated with the segment
           # (last element is the end point; bezier curves also have a control
           # point before last point; the starting point is given by the end
           # point of the last segment)
           points = seg[1..(seg.length - 1)].collect{ |p| array2emu(p) }.collect{ |p| [(p[0]*sx).round, (p[1]*sy).round] }
           verticies += points
           # the last point is the endpoint for the segment that forms a
           # "connection site" with the next segment
           connection_sites << points.last
         end # if

         # add appropriate code to the segment information array indicating the
         # type of segment to create
         seg_info << PATH_SEGMENT_DICTIONARY[type]
       end # each

       @path_verticies        = build_array(verticies.collect{ |v| "(#{v[0]},#{v[1]})" }, 8)
       @path_connection_sites = build_array(connection_sites.collect{ |s| "(#{s[0]},#{s[1]})" }, 8)
       @path_segment_info     = build_array(seg_info, 2)
     end # parse_path()
  end # End of the DocumentProperties class
end # module RRTF