# encoding: utf-8

# core/text/formatted/line_wrap.rb : Implements individual line wrapping of
#                                    formatted text
#
# Copyright February 2010, Daniel Nelson. All Rights Reserved.
#
# This is free software. Please see the LICENSE and COPYING files for details.
#

module Prawn
  module Text
    module Formatted #:nodoc:
      # @private
      class LineWrap #:nodoc:
        # The width of the last wrapped line
        #
        def width
          @accumulated_width || 0
        end

        # The number of spaces in the last wrapped line
        attr_reader :space_count
        attr_reader :soft_hyphen
        attr_reader :zero_width_space

        # Whether this line is the last line in the paragraph
        def paragraph_finished?
          @newline_encountered || is_next_string_newline? || @arranger.finished?
        end

        def tokenize(fragment)
          fragment.scan(scan_pattern)
        end

        # Work in conjunction with the PDF::Formatted::Arranger
        # defined in the :arranger option to determine what formatted text
        # will fit within the width defined by the :width option
        #
        def wrap_line(options)
          initialize_line(options)

          while fragment = @arranger.next_string
            @fragment_output = ""

            fragment.lstrip! if first_fragment_on_this_line?(fragment)
            next if empty_line?(fragment)

            unless apply_font_settings_and_add_fragment_to_line(fragment)
              break
            end
          end
          @arranger.finalize_line
          @accumulated_width = @arranger.line_width
          @space_count = @arranger.space_count
          @arranger.line
        end

        private

        def first_fragment_on_this_line?(fragment)
          line_empty? && fragment != "\n"
        end

        def empty_line?(fragment)
          empty = line_empty? && fragment.empty? && is_next_string_newline?
          @arranger.update_last_string("", "", soft_hyphen) if empty
          empty
        end

        def is_next_string_newline?
          @arranger.preview_next_string == "\n"
        end

        def apply_font_settings_and_add_fragment_to_line(fragment)
          result = nil
          @arranger.apply_font_settings do
            # if font has changed from Unicode to non-Unicode, or vice versa, the characters used for soft hyphens
            #   and zero-width spaces will be different
            set_soft_hyphen_and_zero_width_space
            result = add_fragment_to_line(fragment)
          end
          result
        end

        # returns true if all text was printed without running into the end of
        # the line
        #
        def add_fragment_to_line(fragment)
          if fragment == ""
            true
          elsif fragment == "\n"
            @newline_encountered = true
            false
          else
            tokenize(fragment).each do |segment|
              if segment == zero_width_space
                segment_width = 0
              else
                segment_width = @document.width_of(segment, :kerning => @kerning)
              end

              if @accumulated_width + segment_width <= @width
                @accumulated_width += segment_width
                if segment[-1] == soft_hyphen
                  sh_width = @document.width_of("#{soft_hyphen}", :kerning => @kerning)
                  @accumulated_width -= sh_width
                end
                @fragment_output += segment
              else
                end_of_the_line_reached(segment)
                fragment_finished(fragment)
                return false
              end
            end

            fragment_finished(fragment)
            true
          end
        end

        # The pattern used to determine chunks of text to place on a given line
        #
        def scan_pattern
          pattern = "[^#{break_chars}]+#{soft_hyphen}|" \
            "[^#{break_chars}]+#{hyphen}+|" \
            "[^#{break_chars}]+|" \
            "[#{whitespace}]+|" \
            "#{hyphen}+[^#{break_chars}]*|" \
            "#{soft_hyphen}"

          Regexp.new(pattern)
        end

        # The pattern used to determine whether any word breaks exist on a
        # current line, which in turn determines whether character level
        # word breaking is needed
        #
        def word_division_scan_pattern
          Regexp.new("\\s|[#{zero_width_space}#{soft_hyphen}#{hyphen}]")
        end

        def break_chars
          "#{whitespace}#{soft_hyphen}#{hyphen}"
        end

        def whitespace
          " \\t#{zero_width_space}"
        end

        def hyphen
          "-"
        end

        def line_empty?
          @line_empty && @accumulated_width == 0
        end

        def initialize_line(options)
          @document = options[:document]
          @kerning = options[:kerning]
          @width = options[:width]

          @disable_wrap_by_char = options[:disable_wrap_by_char]

          @accumulated_width = 0
          @line_empty = true
          @line_contains_more_than_one_word = false

          @arranger = options[:arranger]
          @arranger.initialize_line

          @newline_encountered = false
          @line_full = false
        end

        def set_soft_hyphen_and_zero_width_space
          # this is done once per fragment, after the font settings for the fragment are applied --
          #   it could actually be skipped if the font hasn't changed
          font = @document.font
          @soft_hyphen = font.normalize_encoding(Prawn::Text::SHY)
          @zero_width_space = font.unicode? ? Prawn::Text::ZWSP : ""
        end

        def fragment_finished(fragment)
          if fragment == "\n"
            @newline_encountered = true
            @line_empty = false
          else
            update_output_based_on_last_fragment(fragment, soft_hyphen)
            update_line_status_based_on_last_output
            determine_whether_to_pull_preceding_fragment_to_join_this_one(fragment)
          end
          remember_this_fragment_for_backward_looking_ops
        end

        def update_output_based_on_last_fragment(fragment, normalized_soft_hyphen = nil)
          remaining_text = fragment.slice(@fragment_output.length..fragment.length)
          fail Prawn::Errors::CannotFit if line_finished? && line_empty? && @fragment_output.empty? && !fragment.strip.empty?
          @arranger.update_last_string(@fragment_output, remaining_text, normalized_soft_hyphen)
        end

        def determine_whether_to_pull_preceding_fragment_to_join_this_one(current_fragment)
          if @fragment_output.empty? && !current_fragment.empty? && @line_contains_more_than_one_word
            unless previous_fragment_ended_with_breakable? || fragment_begins_with_breakable?(current_fragment)
              @fragment_output = @previous_fragment_output_without_last_word
              update_output_based_on_last_fragment(@previous_fragment)
            end
          end
        end

        def remember_this_fragment_for_backward_looking_ops
          @previous_fragment = @fragment_output.dup
          pf = @previous_fragment
          @previous_fragment_ended_with_breakable = pf =~ /[#{break_chars}]$/
          last_word = pf.slice(/[^#{break_chars}]*$/)
          last_word_length = last_word.nil? ? 0 : last_word.length
          @previous_fragment_output_without_last_word = pf.slice(0, pf.length - last_word_length)
        end

        def previous_fragment_ended_with_breakable?
          @previous_fragment_ended_with_breakable
        end

        def fragment_begins_with_breakable?(fragment)
          fragment =~ /^[#{break_chars}]/
        end

        def line_finished?
          @line_full || paragraph_finished?
        end

        def update_line_status_based_on_last_output
          @line_contains_more_than_one_word = true if @fragment_output =~ word_division_scan_pattern
        end

        def end_of_the_line_reached(segment)
          update_line_status_based_on_last_output
          wrap_by_char(segment) unless @disable_wrap_by_char || @line_contains_more_than_one_word
          @line_full = true
        end

        def wrap_by_char(segment)
          font = @document.font
          segment.each_char do |char|
            break unless append_char(char, font)
          end
        end

        def append_char(char, font)
          # kerning doesn't make sense in the context of a single character
          char_width = font.compute_width_of(char)

          if @accumulated_width + char_width <= @width
            @accumulated_width += char_width
            @fragment_output << char
            true
          else
            false
          end
        end
      end
    end
  end
end
