module Squib
  module Graphics
    # Helper class to generate templated sheet.
    class SaveSprue
      def initialize(deck, tmpl, sheet_args)
        @deck = deck
        @tmpl = tmpl
        @page_number = 0
        @sheet_args = sheet_args # might be Args::Sheet or Args::SaveBatch
        @overlay_lines = @tmpl.crop_lines.select do |line|
          line['overlay_on_cards']
        end
      end

      def render_sheet(range)
        cc = init_cc
        cc.set_source_color(:white) # white backdrop TODO make option
        cc.paint
        slots = @tmpl.cards
        per_sheet = slots.size
        check_oversized_card

        draw_overlay_below_cards cc if range.size

        track_progress(range) do |bar|
          range.each do |i|
            cc = next_page_if_needed(cc, i, per_sheet)

            card = @deck.cards[i]
            slot = slots[i % per_sheet]

            draw_card cc, card,
                      slot['x'] - @sheet_args.trim,
                      slot['y']  - @sheet_args.trim,
                      slot['rotate'],
                      slot['flip_vertical'], slot['flip_horizontal'],
                      @sheet_args.trim, @sheet_args.trim_radius
            bar.increment
          end

          draw_overlay_above_cards cc
          draw_final_page cc # See bug #320
        end
      end

      protected

      # Initialize the Cairo Context
      def init_cc
        raise NotImplementedError
      end

      def draw_page(cc)
        raise NotImplementedError
      end

      def full_filename
        raise NotImplementedError
      end

      private

      def next_page_if_needed(cc, i, per_sheet)
        return cc unless (i != 0) && (i % per_sheet) == 0

        draw_overlay_above_cards cc
        cc = draw_page cc
        draw_overlay_below_cards cc
        @page_number += 1
        cc
      end

      def track_progress(range)
        msg = "Saving templated sheet to #{full_filename}"
        @deck.progress_bar.start(msg, range.size) { |bar| yield(bar) }
      end

      def draw_overlay_below_cards(cc)
        if @tmpl.crop_line_overlay == :on_margin
          add_margin_overlay_clip_mask cc
          cc.clip
          draw_crop_line cc, @tmpl.crop_lines
          cc.reset_clip
        elsif @tmpl.crop_line_overlay == :beneath_cards
          draw_crop_line cc, @tmpl.crop_lines
        end
      end

      def draw_overlay_above_cards(cc)
        if @tmpl.crop_line_overlay == :overlay_on_cards
          draw_crop_line cc, @tmpl.crop_lines
        else
          draw_crop_line cc, @overlay_lines
        end
      end

      def add_margin_overlay_clip_mask(cc)
        margin = @tmpl.margin
        cc.new_path
        cc.rectangle(
          margin[:left], margin[:top],
          margin[:right] - margin[:left],
          margin[:bottom] - margin[:top]
        )
        cc.new_sub_path
        cc.move_to @tmpl.sheet_width, 0
        cc.line_to 0, 0
        cc.line_to 0, @tmpl.sheet_height
        cc.line_to @tmpl.sheet_width, @tmpl.sheet_height
        cc.close_path
      end

      def draw_crop_line(cc, crop_lines)
        crop_lines.each do |line|
          cc.move_to line['line'].x1, line['line'].y1
          cc.line_to line['line'].x2, line['line'].y2
          cc.set_source_color line['color']
          cc.set_line_width line['width']
          cc.set_dash(line['style'].pattern) if line['style'].pattern
          cc.stroke
        end
      end

      def check_oversized_card
        Squib.logger.warn {
          "Card size is larger than sprue's expected card size "\
          "of #{@tmpl.card_width}x#{@tmpl.card_height}. Cards may overlap."
        } if (@deck.width - 2.0 * @sheet_args.trim)  > @tmpl.card_width ||
             (@deck.height - 2.0 * @sheet_args.trim) > @tmpl.card_height
      end

      def draw_card(cc, card, x, y, angle, flip_v, flip_h, trim, trim_radius)
        # Compute the true size of the card after trimming
        w = @deck.width - 2.0 * trim
        h = @deck.height - 2.0 * trim

        # Normalize the angles first
        # TODO do this in the args class
        angle = angle % (2 * Math::PI)
        angle = 2 * Math::PI - angle if angle < 0

        # Perform the actual rotation and drawing
        mat = cc.matrix # Save the transformation matrix to revert later
        cc.translate x, y
        cc.translate @deck.width / 2.0, @deck.height / 2.0
        cc.flip(flip_v, flip_h, 0, 0)
        cc.rotate angle
        cc.translate -@deck.width / 2.0, -@deck.height / 2.0
        cc.rounded_rectangle(trim, trim, w, h, trim_radius, trim_radius) # clip
        cc.clip
        cc.set_source card.cairo_surface, 0, 0
        cc.matrix = mat
        cc.paint
        cc.reset_clip
      end
    end

    # Templated sheet renderer in PDF format.
    class SaveSpruePDF < SaveSprue
      def init_cc
        ratio = 72.0 / @deck.dpi

        slots = @tmpl.cards
        per_sheet = slots.size

        surface = if per_sheet == 1
            Cairo::PDFSurface.new(
                full_filename,
                (@tmpl.sheet_width - 2 * @sheet_args.trim) * ratio,
                (@tmpl.sheet_height - 2 *@sheet_args.trim) * ratio
            )
        else
            Cairo::PDFSurface.new(
                full_filename,
                @tmpl.sheet_width * ratio,
                @tmpl.sheet_height * ratio
            )
        end

        cc = CairoContextWrapper.new(Cairo::Context.new(surface))
        # cc = Cairo::Context.new(surface)
        cc.scale(72.0 / @deck.dpi, 72.0 / @deck.dpi) # make it like pixels
        cc
      end

      def draw_page(cc)
        cc.show_page
        cc.set_source_color(:white) # white backdrop TODO make option
        cc.paint
        cc
      end

      def draw_final_page(cc)
        # PDF doesn't need to create a last page. See bug #320
        cc.target.finish
      end 

      def full_filename
        @sheet_args.full_filename
      end
    end

    # Templated sheet renderer in PNG format.
    class SaveSpruePNG < SaveSprue
      def init_cc
        surface = Cairo::ImageSurface.new @tmpl.sheet_width, @tmpl.sheet_height
        CairoContextWrapper.new(Cairo::Context.new(surface))
        # Cairo::Context.new(surface)
      end

      def draw_page(cc)
        cc.target.write_to_png(full_filename)
        init_cc
        cc.set_source_color(:white) # white backdrop TODO make option
        cc.paint
        cc
      end

      # The last page always gets written out for PNGs because they are separate
      # files and don't get "flushed" automatically. See bug #320.
      def draw_final_page(cc)
        draw_page cc
        cc.target.finish
      end

      def full_filename
        @sheet_args.full_filename @page_number
      end
    end
  end
end
