# $Id: misc.rb,v 1.17 2010/03/21 01:43:01 baror Exp $
# Copyright (C) 2009 Timothy P. Hunter
module Magick
  class RVG
    # This is a standard deep_copy method that is used in most classes.
    # Thanks to Robert Klemme.
    module Duplicatable
      def deep_copy(h = {})
        # Prevent recursion. If we reach the
        # object we started with, stop copying.
        copy = h[__id__]
        unless copy
          h[__id__] = copy = self.class.allocate
          ivars = instance_variables
          ivars.each do |ivar|
            ivalue = instance_variable_get(ivar)
            cvalue = case
              when NilClass === ivalue, Symbol === ivalue, Float === ivalue,
                 Fixnum === ivalue, FalseClass === ivalue, TrueClass === ivalue
                ivalue
              when ivalue.respond_to?(:deep_copy)
                ivalue.deep_copy(h)
              when ivalue.respond_to?(:dup)
                ivalue.dup
              else
                ivalue
              end
            copy.instance_variable_set(ivar, cvalue)
          end
          copy.freeze if frozen?
        end
        copy
      end
    end     # module Duplicatable

    # Convert an array of method arguments to Float objects. If any
    # cannot be converted, raise ArgumentError and issue a message.
    def self.fmsg(*args)
      "at least one argument cannot be converted to Float (got #{args.collect {|a| a.class}.join(', ')})"
    end

    def self.convert_to_float(*args)
      allow_nil = false
      if args.last == :allow_nil
        allow_nil = true
        args.pop
      end
      begin
        fargs = args.collect { |a| (allow_nil && a.nil?) ? a : Float(a) }
      rescue ArgumentError, TypeError
        raise ArgumentError, fmsg(*args)
      end
      fargs
    end

    def self.convert_one_to_float(arg)
      begin
        farg = Float(arg)
      rescue ArgumentError, TypeError
        raise ArgumentError, "argument cannot be converted to Float (got #{arg.class})"
      end
      farg
    end
  end # class RVG
end # module Magick

module Magick
  class RVG
    class Utility
      class TextStrategy
        def initialize(context)
          @ctx = context
          @ctx.shadow.affine = @ctx.text_attrs.affine
        end

        def enquote(text)
          if text.length > 2 && /\A(?:\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})\z/.match(text)
            return text
          elsif !text['\'']
            text = '\''+text+'\''
            return text
          elsif !text['"']
            text = '"'+text+'"'
            return text
          elsif !(text['{'] || text['}'])
            text = '{'+text+'}'
            return text
          end

          # escape existing braces, surround with braces
          text.gsub!(/[}]/) { |b| '\\' + b }
          '{' +  text + '}'
        end

        def glyph_metrics(glyph_orientation, glyph)
          gm = @ctx.shadow.get_type_metrics('a' + glyph + 'a')
          gm2 = @ctx.shadow.get_type_metrics('aa')
          h = (gm.ascent - gm.descent + 0.5).to_i
          w = gm.width - gm2.width
          if glyph_orientation == 0 || glyph_orientation == 180
            [w, h]
          else
            [h, w]
          end
        end

        def text_rel_coords(text)
          y_rel_coords = []
          x_rel_coords = []
          first_word = true
          words = text.split(::Magick::RVG::WORD_SEP)
          words.each do |word|
            unless first_word
              wx, wy = get_word_spacing
              x_rel_coords << wx
              y_rel_coords << wy
            end
            first_word = false
            word.split('').each do |glyph|
              wx, wy = get_letter_spacing(glyph)
              x_rel_coords << wx
              y_rel_coords << wy
            end
          end
          [x_rel_coords, y_rel_coords]
        end

        def shift_baseline(glyph_orientation, glyph)
          glyph_dimensions = @ctx.shadow.get_type_metrics(glyph)
          if glyph_orientation == 0 || glyph_orientation == 180
            x = glyph_dimensions.width
          else
            x = glyph_dimensions.ascent - glyph_dimensions.descent
          end
          case @ctx.text_attrs.baseline_shift
            when :baseline
              x = 0
            when :sub

            when :super
              x = -x
            when /[-+]?(\d+)%/
              m = $1 == '-' ? -1.0 : 1.0
              x = (m * x * $1.to_f / 100.0)
            else
              x = -@ctx.text_attrs.baseline_shift
          end
          x
        end

        def render_glyph(glyph_orientation, x, y, glyph)
          if glyph_orientation == 0
            @ctx.gc.text(x, y, enquote(glyph))
          else
            @ctx.gc.push
            @ctx.gc.translate(x, y)
            @ctx.gc.rotate(glyph_orientation)
            @ctx.gc.translate(-x, -y)
            @ctx.gc.text(x, y, enquote(glyph))
            @ctx.gc.pop
          end
        end
      end     # class TextStrategy

      class LRTextStrategy < TextStrategy
        def get_word_spacing
          @word_space ||= glyph_metrics(@ctx.text_attrs.glyph_orientation_horizontal, ' ')[0]
          [@word_space + @ctx.text_attrs.word_spacing, 0]
        end

        def get_letter_spacing(glyph)
          gx, gy = glyph_metrics(@ctx.text_attrs.glyph_orientation_horizontal, glyph)
          [gx+@ctx.text_attrs.letter_spacing, gy]
        end

        def render(x, y, text)
          x_rel_coords, y_rel_coords = text_rel_coords(text)
          dx = x_rel_coords.inject(0) {|sum, a| sum + a}
          dy = y_rel_coords.max

          # We're handling the anchoring.
          @ctx.gc.push
          @ctx.gc.text_anchor(Magick::StartAnchor)
          if @ctx.text_attrs.text_anchor == :end
            x -= dx
          elsif @ctx.text_attrs.text_anchor == :middle
            x -= dx / 2
          end

          # Align the first glyph
          case @ctx.text_attrs.glyph_orientation_horizontal
            when 0

            when 90
              y -= dy
            when 180
              x += x_rel_coords.shift
              x_rel_coords << 0
              y -= dy
            when 270
              x += x_rel_coords[0]
          end

          y += shift_baseline(@ctx.text_attrs.glyph_orientation_horizontal, text[0,1])

          first_word = true
          text.split(::Magick::RVG::WORD_SEP).each do |word|
            unless first_word
              x += x_rel_coords.shift
            end
            first_word = false
            word.split('').each do |glyph|
              render_glyph(@ctx.text_attrs.glyph_orientation_horizontal, x, y, glyph)
              x += x_rel_coords.shift
            end
          end

          @ctx.gc.pop
          [dx, 0]
        end
      end     # class LRTextStrategy

      class RLTextStrategy < TextStrategy
        def render(x, y, text)
          fail NotImplementedError
        end
      end     # class RLTextStrategy

      class TBTextStrategy < TextStrategy
        def get_word_spacing
          @word_space ||= glyph_metrics(@ctx.text_attrs.glyph_orientation_vertical, ' ')[1]
          [0, @word_space + @ctx.text_attrs.word_spacing]
        end

        def get_letter_spacing(glyph)
          gx, gy = glyph_metrics(@ctx.text_attrs.glyph_orientation_vertical, glyph)
          [gx, gy+@ctx.text_attrs.letter_spacing]
        end

        def render(x, y, text)
          x_rel_coords, y_rel_coords = text_rel_coords(text)
          dx = x_rel_coords.max
          dy = y_rel_coords.inject(0) {|sum, a| sum + a}

          # We're handling the anchoring.
          @ctx.gc.push
          @ctx.gc.text_anchor(Magick::StartAnchor)
          if @ctx.text_attrs.text_anchor == :end
            y -= dy
          elsif @ctx.text_attrs.text_anchor == :middle
            y -= dy / 2
          end

          # Align the first glyph such that its center
          # is aligned on x and its top is aligned on y.

          case @ctx.text_attrs.glyph_orientation_vertical
            when 0
              x -= x_rel_coords.max / 2
              y += y_rel_coords[0]
            when 90
              x -= x_rel_coords.max / 2
            when 180
              x += x_rel_coords.max / 2
            when 270
              x += x_rel_coords.max / 2
              y += y_rel_coords.shift
              y_rel_coords << 0   # since we used an element we need to add a dummy
          end

          x -= shift_baseline(@ctx.text_attrs.glyph_orientation_vertical, text[0,1])

          first_word = true
          text.split(::Magick::RVG::WORD_SEP).each do |word|
            unless first_word
              y += y_rel_coords.shift
              x_rel_coords.shift
            end
            first_word = false
            word.split('').each do |glyph|
              case @ctx.text_attrs.glyph_orientation_vertical.to_i
                when 0, 90, 270
                  x_shift = (dx - x_rel_coords.shift) / 2
                when 180
                  x_shift = -(dx - x_rel_coords.shift) / 2
              end

              render_glyph(@ctx.text_attrs.glyph_orientation_vertical, x+x_shift, y, glyph)
              y += y_rel_coords.shift
            end
          end

          @ctx.gc.pop
          [0, dy]
        end
      end     # class TBTextStrategy

      # Handle "easy" text
      class DefaultTextStrategy < TextStrategy
        def render(x, y, text)
          @ctx.gc.text(x, y, enquote(text))
          tm = @ctx.shadow.get_type_metrics(text)
          dx = case @ctx.text_attrs.text_anchor
              when :start
                 tm.width
              when :middle
                 tm.width / 2
              when :end
                 0
              end
          [dx, 0]
        end
      end     # class NormalTextStrategy
    end # class Utility
  end # class RVG
end # module Magick

module Magick
  class RVG
    class Utility
      class TextAttributes
        public

        WRITING_MODE = %w{lr-tb lr rl-tb rl tb-rl tb}

        def initialize
          @affine = []
          @affine << Magick::AffineMatrix.new(1, 0, 0, 1, 0, 0)
          @baseline_shift = []
          @baseline_shift << :baseline
          @glyph_orientation_horizontal = []
          @glyph_orientation_horizontal << 0
          @glyph_orientation_vertical = []
          @glyph_orientation_vertical << 90
          @letter_spacing = []
          @letter_spacing << 0
          @text_anchor = []
          @text_anchor << :start
          @word_spacing = []
          @word_spacing << 0
          @writing_mode = []
          @writing_mode << 'lr-tb'
        end

        def push
          @affine.push(@affine.last.dup)
          @baseline_shift.push(@baseline_shift.last)
          @text_anchor.push(@text_anchor.last)
          @writing_mode.push(@writing_mode.last.dup)
          @glyph_orientation_vertical.push(@glyph_orientation_vertical.last)
          @glyph_orientation_horizontal.push(@glyph_orientation_horizontal.last)
          @letter_spacing.push(@letter_spacing.last)
          @word_spacing.push(@word_spacing.last)
        end

        def pop
          @affine.pop
          @baseline_shift.pop
          @text_anchor.pop
          @writing_mode.pop
          @glyph_orientation_vertical.pop
          @glyph_orientation_horizontal.pop
          @letter_spacing.pop
          @word_spacing.pop
        end

        def set_affine(sx, rx, ry, sy, tx, ty)
          @affine[-1].sx = sx
          @affine[-1].rx = rx
          @affine[-1].ry = ry
          @affine[-1].sy = sy
          @affine[-1].tx = tx
          @affine[-1].ty = ty
        end

        def affine
          @affine[-1]
        end

        def baseline_shift
          @baseline_shift[-1]
        end

        def baseline_shift=(value)
          @baseline_shift[-1] = value
        end

        def text_anchor
          @text_anchor[-1]
        end

        def text_anchor=(anchor)
          @text_anchor[-1] = anchor
        end

        def glyph_orientation_vertical
          @glyph_orientation_vertical[-1]
        end

        def glyph_orientation_vertical=(angle)
          @glyph_orientation_vertical[-1] = angle
        end

        def glyph_orientation_horizontal
          @glyph_orientation_horizontal[-1]
        end

        def glyph_orientation_horizontal=(angle)
          @glyph_orientation_horizontal[-1] = angle
        end

        def letter_spacing
          @letter_spacing[-1]
        end

        def letter_spacing=(value)
          @letter_spacing[-1] = value
        end

        def non_default?
          @baseline_shift[-1] != :baseline || @letter_spacing[-1] != 0 ||
          @word_spacing[-1] != 0 || @writing_mode[-1][/\Alr/].nil? ||
          @glyph_orientation_horizontal[-1] != 0
        end

        def word_spacing
          @word_spacing[-1]
        end

        def word_spacing=(value)
          @word_spacing[-1] = value
        end

        def writing_mode
          @writing_mode[-1]
        end

        def writing_mode=(mode)
          @writing_mode[-1] = WRITING_MODE.include?(mode) ? mode : 'lr-tb'
        end
      end     # class TextAttributes

      class GraphicContext
        FONT_STRETCH = {
          :normal => Magick::NormalStretch,
          :ultra_condensed => Magick::UltraCondensedStretch,
          :extra_condensed => Magick::ExtraCondensedStretch,
          :condensed       => Magick::CondensedStretch,
          :semi_condensed  => Magick::SemiCondensedStretch,
          :semi_expanded   => Magick::SemiExpandedStretch,
          :expanded        => Magick::ExpandedStretch,
          :extra_expanded  => Magick::ExtraExpandedStretch,
          :ultra_expanded  => Magick::UltraExpandedStretch
        }

        FONT_STYLE = {
          :normal  => Magick::NormalStyle,
          :italic  => Magick::ItalicStyle,
          :oblique => Magick::ObliqueStyle
        }

        FONT_WEIGHT = {
          'normal'  => Magick::NormalWeight,
          'bold'    => Magick::BoldWeight,
          'bolder'  => Magick::BolderWeight,
          'lighter' => Magick::LighterWeight
        }

        TEXT_ANCHOR = {
          :start  => Magick::StartAnchor,
          :middle => Magick::MiddleAnchor,
          :end    => Magick::EndAnchor
        }

        ANCHOR_TO_ALIGN = {
          :start  => Magick::LeftAlign,
          :middle => Magick::CenterAlign,
          :end    => Magick::RightAlign
        }

        TEXT_DECORATION = {
          :none         => Magick::NoDecoration,
          :underline    => Magick::UnderlineDecoration,
          :overline     => Magick::OverlineDecoration,
          :line_through => Magick::LineThroughDecoration}

        TEXT_STRATEGIES  = {
          'lr-tb' => LRTextStrategy, 'lr' => LRTextStrategy,
          'rt-tb' => RLTextStrategy, 'rl' => RLTextStrategy,
          'tb-rl' => TBTextStrategy, 'tb' => TBTextStrategy
        }

        def self.degrees_to_radians(deg)
          Math::PI * (deg % 360.0) / 180.0
        end

        private

        def init_matrix
          @rx = @ry = 0
          @sx = @sy = 1
          @tx = @ty = 0
        end

        def concat_matrix
          curr = @text_attrs.affine
          sx = curr.sx * @sx + curr.ry * @rx
          rx = curr.rx * @sx + curr.sy * @rx
          ry = curr.sx * @ry + curr.ry * @sy
          sy = curr.rx * @ry + curr.sy * @sy
          tx = curr.sx * @tx + curr.ry * @ty + curr.tx
          ty = curr.rx * @tx + curr.sy * @ty + curr.ty
          @text_attrs.set_affine(sx, rx, ry, sy, tx, ty)
          init_matrix
        end

        public

        attr_reader :gc, :text_attrs

        def initialize
          @gc = Magick::Draw.new
          @shadow = []
          @shadow << Magick::Draw.new
          @text_attrs = TextAttributes.new
          init_matrix
        end

        def method_missing(methID, *args, &block)
          @gc.__send__(methID, *args, &block)
        end

        def affine(sx, rx, ry, sy, tx, ty)
          sx, rx, ry, sy, tx, ty = Magick::RVG.convert_to_float(sx, rx, ry, sy, tx, ty)
          @gc.affine(sx, rx, ry, sy, tx, ty)
          @text_attrs.set_affine(sx, rx, ry, sy, tx, ty)
          nil
        end

        def baseline_shift(value)
          @text_attrs.baseline_shift = case value
            when 'baseline', 'sub', 'super'
              value.intern
            when /[-+]?\d+%/, Numeric
              value
            else
              :baseline
            end
          nil
        end

        def font(name)
          @gc.font(name)
          @shadow[-1].font = name
          nil
        end

        def font_family(name)
          @gc.font_family(name)
          @shadow[-1].font_family = name
          nil
        end

        def font_size(points)
          @gc.font_size(points)
          @shadow[-1].pointsize = points
          nil
        end

        def font_stretch(stretch)
          stretch = FONT_STRETCH.fetch(stretch.intern, Magick::NormalStretch)
          @gc.font_stretch(stretch)
          @shadow[-1].font_stretch = stretch
          nil
        end

        def font_style(style)
          style = FONT_STYLE.fetch(style.intern, Magick::NormalStyle)
          @gc.font_style(style)
          @shadow[-1].font_style = style
          nil
        end

        def font_weight(weight)
          # If the arg is not in the hash use it directly. Handles numeric values.
          weight = FONT_WEIGHT.fetch(weight) {|key| key}
          @gc.font_weight(weight)
          @shadow[-1].font_weight = weight
          nil
        end

        def glyph_orientation_horizontal(deg)
          deg = Magick::RVG.convert_one_to_float(deg)
          @text_attrs.glyph_orientation_horizontal = (deg % 360) / 90 * 90
          nil
        end

        def glyph_orientation_vertical(deg)
          deg = Magick::RVG.convert_one_to_float(deg)
          @text_attrs.glyph_orientation_vertical = (deg % 360) / 90 * 90
          nil
        end

        def inspect
          @gc.inspect
        end

        def letter_spacing(value)
          @text_attrs.letter_spacing = Magick::RVG.convert_one_to_float(value)
          nil
        end

        def push
          @gc.push
          @shadow.push(@shadow.last.dup)
          @text_attrs.push
          nil
        end

        def pop
          @gc.pop
          @shadow.pop
          @text_attrs.pop
          nil
        end

        def rotate(degrees)
          degrees = Magick::RVG.convert_one_to_float(degrees)
          @gc.rotate(degrees)
          @sx =  Math.cos(GraphicContext.degrees_to_radians(degrees))
          @rx =  Math.sin(GraphicContext.degrees_to_radians(degrees))
          @ry = -Math.sin(GraphicContext.degrees_to_radians(degrees))
          @sy =  Math.cos(GraphicContext.degrees_to_radians(degrees))
          concat_matrix
          nil
        end

        def scale(sx, sy)
          sx, sy = Magick::RVG.convert_to_float(sx, sy)
          @gc.scale(sx, sy)
          @sx = sx
          @sy = sy
          concat_matrix
          nil
        end

        def shadow
          @shadow.last
        end

        def skewX(degrees)
          degrees = Magick::RVG.convert_one_to_float(degrees)
          @gc.skewX(degrees)
          @ry = Math.tan(GraphicContext.degrees_to_radians(degrees))
          concat_matrix
          nil
        end

        def skewY(degrees)
          degrees = Magick::RVG.convert_one_to_float(degrees)
          @gc.skewY(degrees)
          @rx = Math.tan(GraphicContext.degrees_to_radians(degrees))
          concat_matrix
          nil
        end

        def stroke_width(width)
          width = Magick::RVG.convert_one_to_float(width)
          @gc.stroke_width(width)
          @shadow[-1].stroke_width = width
          nil
        end

        def text(x, y, text)
          return if text.length == 0
          if @text_attrs.non_default?
            text_renderer = TEXT_STRATEGIES[@text_attrs.writing_mode].new(self)
          else
            text_renderer = DefaultTextStrategy.new(self)
          end

          text_renderer.render(x, y, text)
        end

        def text_anchor(anchor)
          anchor = anchor.intern
          anchor_enum = TEXT_ANCHOR.fetch(anchor, Magick::StartAnchor)
          @gc.text_anchor(anchor_enum)
          align = ANCHOR_TO_ALIGN.fetch(anchor, Magick::LeftAlign)
          @shadow[-1].align = align
          @text_attrs.text_anchor = anchor
          nil
        end

        def text_decoration(decoration)
          decoration = TEXT_DECORATION.fetch(decoration.intern, Magick::NoDecoration)
          @gc.decorate(decoration)
          @shadow[-1].decorate = decoration
          nil
        end

        def translate(tx, ty)
          tx, ty = Magick::RVG.convert_to_float(tx, ty)
          @gc.translate(tx, ty)
          @tx = tx
          @ty = ty
          concat_matrix
          nil
        end

        def word_spacing(value)
          @text_attrs.word_spacing = Magick::RVG.convert_one_to_float(value)
          nil
        end

        def writing_mode(mode)
          @text_attrs.writing_mode = mode
          nil
        end
      end  # class GraphicContext
    end # class Utility
  end # class RVG
end # module Magick
