# This regular expression is used for splitting a string into wrappable words
WORD_RE = /([^ ,\/!.?:;\-\n]+[ ,\/!.?:;\-]*)|\n/g

module.exports = 
    initText: ->
        # Current coordinates
        @x = 0
        @y = 0
        @_lineGap = 0
        
        # Keeps track of what has been set in the document
        @_textState = 
            mode: 0
            wordSpacing: 0
            characterSpacing: 0
            
        # state of the wrapping algorithm
        @_wrapState = {}
        
    lineGap: (@_lineGap) ->
        return this
        
    _initOptions: (x = {}, y, options = {}) ->
        if typeof x is 'object'
            options = x
            x = null
            
        # clone options object
        options = do ->
            opts = {}
            opts[k] = v for k, v of options
            return opts
            
        # Update the current position
        if x? or y?
            @x = x or @x
            @y = y or @y

        # wrap to margins if no x or y position passed
        else
            margins = @page.margins
            options.width ?= @page.width - @x - margins.right
            options.height ?= @page.height - @y - margins.bottom

        # wrap to columns
        options.columns ||= 1
        options.columnGap ?= 18 # 1/4 inch
        
        return options
        
    text: (text, x = {}, y, options = {}) ->
        options = @_initOptions(x, y, options)
                    
        # Convert text to a string
        text = '' + text
                    
        # if the wordSpacing option is specified, remove multiple consecutive spaces
        if options.wordSpacing
            text = text.replace(/\s+/g, ' ')
            
        paragraphs = text.split '\n'
        
        # word wrapping
        if options.width
            @_wrap paragraphs, options
            
        # render paragraphs as lines
        else
            @_line line, options for line in paragraphs
        
        return this
        
        # word wrapping
        if options.width
            @_wrap text, options
        
        # newlines    
        else if (matches = text.split '\n').length > 1
            @_line match, options for match in matches
        
        # single line    
        else
            @_line text, options
            
        return this
        
    moveDown: (lines = 1) ->
        @y += @currentLineHeight(true) * lines + @_lineGap
        return this

    moveUp: (lines = 1) ->
        @y -= @currentLineHeight(true) * lines + @_lineGap
        return this

    list: (array, x = {}, y, options = {}) ->
        options = @_initOptions(x, y, options)
            
        r = ((@_font.ascender / 1000 * @_fontSize) / 3) | 0
        
        @x += r * 6
        
        options.listType = 'bullet'
        @_wrap array, options
        
        return
        
        for item, i in array
            # Convert text to a string
            # text = '' + text
            
            @circle @x - r * 5, @y + r * 2 + 1, r
            @text item, options
            @y += (options.paragraphGap) or 0
        
        @x = x    
        @fill()
        
        ###
        gap = Math.round (@_font.ascender / 1000 * @_fontSize) / 2
        @x = x = ox or @x
        @y = y = oy or @y

        for item in array
            @circle x + 3, @y + gap + 3, 3
            @text item, x + 15
            @y += 3
            
        @x = x
        @fill()
        ###
            
    _line: (text, options) ->
        wrap = @_wrapState
        paragraphGap = (wrap.firstLine and @y isnt wrap.startY and options.paragraphGap) or 0
        lineGap = options.lineGap or @_lineGap or 0
        
        @_fragment text, @x, @y + paragraphGap, options
        @y += @currentLineHeight(true) + lineGap + paragraphGap
            
    _fragment: (text, x, y, options = {}) ->
        text = '' + text
        return if text.length is 0
        
        state = @_textState
        wrap = @_wrapState
        
        # handle options
        align = options.align or 'left'
        indent = (wrap.firstLine and options.indent) or 0
        wordSpacing = options.wordSpacing or 0
        characterSpacing = options.characterSpacing or 0
        
        # text alignments
        if options.width
            lineWidth = wrap.lineWidth - indent - wrap.extraSpace
            
            switch align
                when 'right'
                    x += lineWidth - @widthOfString(text)
                
                when 'center'
                    x += lineWidth / 2 - @widthOfString(text) / 2
                
                when 'justify'
                    # don't justify the last line of paragraphs
                    break if wrap.lastLine
                    
                    # split the line into words
                    words = text.match(WORD_RE)
                    break unless words
                     
                    # calculate the word spacing value                
                    textWidth = @widthOfString text.replace(/\s+/g, '')
                    wordSpacing = (lineWidth - textWidth) / (words.length - 1) - @widthOfString(' ')
                    
        # indentation support
        x += indent
        
        # flip coordinate system
        y = @page.height - y - (@_font.ascender / 1000 * @_fontSize)
        
        # add current font to page if necessary
        @page.fonts[@_font.id] ?= @_font.ref
        
        # tell the font subset to use the characters
        @_font.use text
        
        # encode the text based on the font subset,
        # and then convert it to hex
        text = @_font.encode text
        text = (text.charCodeAt(i).toString(16) for i in [0...text.length]).join('')
        
        # begin the text object
        @addContent "BT"
        
        # text position
        @addContent "#{x} #{y} Td"
        
        # font and font size
        @addContent "/#{@_font.id} #{@_fontSize} Tf"
        
        # rendering mode
        mode = if options.fill and options.stroke then 2 else if options.stroke then 1 else 0
        @addContent "#{mode} Tr" unless mode is state.mode
        
        # Word spacing
        @addContent wordSpacing + ' Tw' unless wordSpacing is state.wordSpacing
        
        # Character spacing
        @addContent characterSpacing + ' Tc' unless characterSpacing is state.characterSpacing
        
        # add the actual text
        @addContent "<#{text}> Tj"
        
        # end the text object
        @addContent "ET"
        
        # keep track of text states
        state.mode = mode
        state.wordSpacing = wordSpacing

    _wrap: (paragraphs, options) ->
        wrap = @_wrapState
        width = @widthOfString.bind this
        indent = options.indent or 0
        lineWidth = (options.width - (options.columnGap * (options.columns - 1))) / options.columns
        
        # initial settings
        wrap.column = 1             # the current column
        wrap.startY = @y            # the initial Y position
        wrap.lineWidth = lineWidth  # the maximum width of each line
        wrap.firstLine = true       # whether we are on the first line of a paragraph
        wrap.lastLine = false       # whether we are on the last line of a paragraph
        
        # calculate the maximum Y position the text can appear at
        wrap.maxY = @y + options.height - @currentLineHeight()
        
        for text in paragraphs
            wrap.firstLine = true
            
            # split the line into words
            words = text.match(WORD_RE)
        
            # calculate the extra width
            wrap.extraSpace = (options.wordSpacing or 0) * (words.length - 1) +   # wordSpacing
                              (options.characterSpacing or 0) * (text.length - 1) # characterSpacing
        
            # space left on the line to fill with words
            spaceLeft = lineWidth - indent - wrap.extraSpace
        
            # word width cache
            wordWidths = {}
            len = words.length
            buffer = ''
        
            for word, i in words
                w = wordWidths[word] ?= width(word)
            
                if w > spaceLeft
                    # render the line
                    @_line buffer.trim(), options
                
                    # we're no longer on the first line...
                    wrap.firstLine = false
                
                    # if we've reached the maximum height, make sure
                    # that the first line of a paragraph is never by 
                    # itself at the bottom of a page
                    nextY = @y + @currentLineHeight(true)
                    if @y > wrap.maxY or (wrap.lastLine and nextY > wrap.maxY)
                        # if we've reached the edge of the page, 
                        # continue on a new page or column
                        @_nextSection options
                
                    # reset the space left and buffer
                    spaceLeft = lineWidth - w - wrap.extraSpace
                    buffer = if word is '\n' then '' else word

                else
                    # add the word to the buffer
                    spaceLeft -= w
                    buffer += word

            # add the last line
            wrap.lastLine = true
            @_line buffer.trim(), options
        
        # reset wrap state
        @_wrapState = {}
        
    _nextSection: (options) ->
        wrap = @_wrapState
        
        if ++wrap.column > options.columns
            @addPage()
            wrap.column = 1
            wrap.startY = @page.margins.top
            wrap.maxY = @page.maxY()
        
        else
            @x += wrap.lineWidth + options.columnGap
            @y = wrap.startY