# frozen_string_literal: true

# Copyright (c) 2002 - 2009 Timothy P. Hunter
# Copyright (c) 2009 -      RMagick contributors

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 = if ivalue.is_a?(NilClass) || ivalue.is_a?(Symbol) || ivalue.is_a?(Float) || ivalue.is_a?(Integer) || ivalue.is_a?(FalseClass) || ivalue.is_a?(TrueClass)
                       ivalue
                     elsif ivalue.respond_to?(:deep_copy)
                       ivalue.deep_copy(h)
                     elsif 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.map(&: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.map { |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)
          return text if text.length > 2 && /\A(?:"[^"]+"|'[^']+'|\{[^}]+\})\z/.match(text)

          if !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.zero? || 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.chars.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)
          x = if glyph_orientation.zero? || glyph_orientation == 180
                glyph_dimensions.width
              else
                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 = Regexp.last_match(1) == '-' ? -1.0 : 1.0
            x = (m * x * Regexp.last_match(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.zero?
            @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.sum
          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 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|
            x += x_rel_coords.shift unless first_word
            first_word = false
            word.chars.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.sum

          # 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.chars.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
        WRITING_MODE = %w[lr-tb lr rl-tb rl tb-rl tb].freeze

        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
        }.freeze

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

        FONT_WEIGHT = {
          normal: Magick::NormalWeight,
          bold: Magick::BoldWeight,
          bolder: Magick::BolderWeight,
          lighter: Magick::LighterWeight
        }.freeze

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

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

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

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

        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(meth_id, *args, &)
          @gc.__send__(meth_id, *args, &)
        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.to_sym
                                       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.to_sym, Magick::NormalStretch)
          @gc.font_stretch(stretch)
          @shadow[-1].font_stretch = stretch
          nil
        end

        def font_style(style)
          style = FONT_STYLE.fetch(style.to_sym, 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.to_sym, Magick::NormalWeight) unless weight.is_a?(Numeric)
          @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.empty?

          text_renderer = if @text_attrs.non_default?
                            TEXT_STRATEGIES[@text_attrs.writing_mode].new(self)
                          else
                            DefaultTextStrategy.new(self)
                          end

          text_renderer.render(x, y, text)
        end

        def text_anchor(anchor)
          anchor = anchor.to_sym
          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.to_sym, 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
