# $Id: misc.rb,v 1.9 2005/12/31 14:41:04 rmagick Exp $
# Copyright (C) 2006 Timothy P. Hunter
class Magick::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
            return copy
        end

    end     # module Magick::RVG::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, self.fmsg(*args)
        end
        return 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
        return farg
    end

end     # class Magick::RVG





module Magick::RVG::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 }
            return '{' +  text + '}'
        end

        def glyph_metrics(glyph_orientation, glyph)
            glyph_metrics = @ctx.shadow.get_type_metrics(glyph)
            h = glyph_metrics.ascent - glyph_metrics.descent
            w = glyph_metrics.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
            return 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)
            raise 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
                        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     # module Magick::RVG::Utility




module Magick::RVG::Utility

    class TextAttributes

      public

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

        def initialize()
            @affine = Array.new
            @affine << Magick::AffineMatrix.new(1, 0, 0, 1, 0, 0)
            @baseline_shift = Array.new
            @baseline_shift << :baseline
            @glyph_orientation_horizontal = Array.new
            @glyph_orientation_horizontal << 0
            @glyph_orientation_vertical = Array.new
            @glyph_orientation_vertical << 90
            @letter_spacing = Array.new
            @letter_spacing << 0
            @text_anchor = Array.new
            @text_anchor << :start
            @word_spacing = Array.new
            @word_spacing << 0
            @writing_mode = Array.new
            @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 GraphicContext.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 = Array.new
            @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, @sy = sx, 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

            return 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, @ty = tx, 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     # module Magick::RVG::Utility

