# syntax highlighting

require_relative 'common'

module HH::Markup

  TOKENIZER = HH::Syntax.load "ruby"
  COLORS = {
    :comment => {:stroke => "#887"},
    :keyword => {:stroke => "#111"},
    :method => {:stroke => "#C09", :weight => "bold"},
    # :class => {:stroke => "#0c4", :weight => "bold"},
    # :module => {:stroke => "#050"},
    # :punct => {:stroke => "#668", :weight => "bold"},
    :symbol => {:stroke => "#C30"},
    :string => {:stroke => "#C90"},
    :number => {:stroke => "#396" },
    :regex => {:stroke => "#000", :fill => "#FFC" },
    # :char => {:stroke => "#f07"},
    :attribute => {:stroke => "#369" },
    # :global => {:stroke => "#7FB" },
    :expr => {:stroke => "#722" },
    # :escape => {:stroke => "#277" }
    :ident => {:stroke => "#994c99"},
    :constant => {:stroke => "#630", :weight => "bold"},
    :class => {:stroke => "#630", :weight => "bold"},
    :matching => {:stroke => "#ff0", :weight => "bold"},
  }

  
  def highlight str, pos=nil, colors=COLORS
    tokens = []
    TOKENIZER.tokenize(str) do |t|
      if t.group == :punct
        # split punctuation into single characters tokens
        # TODO: to it in the parser
        tokens += t.split('').map{|s| HH::Syntax::Token.new(s, :punct)}
      else
        # add token as is
        tokens << t
      end
    end

    res = []
    tokens.each do |token|
      res <<
        if colors[token.group]
          #span(token, colors[token.group])
          tmp = fg(token, tr_color(colors[token.group][:stroke]))
          colors[token.group][:fill] ? bg(tmp, tr_color(colors[token.group][:fill])) : tmp
        elsif colors[:any]
          #span(token, colors[:any])
          tmp = fg(token, tr_color(colors[:any][:stroke]))
          colors[:any][:fill] ? bg(tmp, tr_color(colors[:any][:fill])) : tmp
        else
          token
        end
    end

    if pos.nil? or pos < 0
      return res.join
    end

    token_index, matching_index = matching_token(tokens, pos)

    if token_index
      #res[token_index] = span(tokens[token_index], colors[:matching])
      tmp = fg(tokens[token_index], tr_color(colors[:matching][:stroke]))
      res[token_index] = colors[:matching][:fill] ? bg(tmp, tr_color(colors[:matching][:fill])) : tmp
      if matching_index
        #res[matching_index] = span(tokens[matching_index], colors[:matching])
        tmp = fg(tokens[matching_index], tr_color(colors[:matching][:stroke]))
        res[matching_index] = colors[:matching][:fill] ? bg(tmp, tr_color(colors[:matching][:fill])) : tmp
      end
    end

    res.join
  end

private
  def matching_token(tokens, pos)
    curr_pos = 0
    token_index = nil
    tokens.each_with_index do |t, i|
      curr_pos += t.size
      if token_index.nil? and curr_pos >= pos
        token_index = i
        break
      end
    end
    if token_index.nil? then return nil end

    match = matching_token_at_index(tokens, token_index);
    if match.nil? and curr_pos == pos and token_index < tokens.size-1
      # try the token before the cursor, instead of the one after
      token_index += 1
      match = matching_token_at_index(tokens, token_index)
    end

    if match
      [token_index, match]
    else
      nil
    end
  end


  def matching_token_at_index(tokens, index)
    starts, ends, direction = matching_tokens(tokens, index)
    if starts.nil?
      return nil
    end

    stack_level = 1
    index += direction
    while index >= 0 and index < tokens.size
      # TODO separate space in the tokenizer
      t = tokens[index].gsub(/\s/, '')
      if ends.include?(t) and not as_modifier?(tokens, index)
        stack_level -= 1
        return index if stack_level == 0
      elsif starts.include?(t) and not as_modifier?(tokens, index)
        stack_level += 1
      end
      index += direction
    end
    # no matching token found
    return nil
  end

  # returns an array of tokens matching and the direction
  def matching_tokens(tokens, index)
    # TODO separate space in the tokenizer
    token = tokens[index].gsub(/\s/, '')
    starts = [token]
    if OPEN_BRACKETS[token]
      direction = 1
      ends = [OPEN_BRACKETS[token]]
    elsif CLOSE_BRACKETS[token]
      direction = -1
      ends = [CLOSE_BRACKETS[token]]
    elsif OPEN_BLOCK.include?(token)
      if as_modifier?(tokens, index)
        return nil
      end
      direction = 1
      ends = ['end']
      starts = OPEN_BLOCK
    elsif token == 'end'
      direction = -1
      ends = OPEN_BLOCK
    else
      return nil
    end

    [starts, ends, direction]
  end

  def as_modifier?(tokens, index)

    if not MODIFIERS.include? tokens[index].gsub(/\s/, '')
      return false
    end
    
    index -= 1
    # find last index before the token that is no space
    index -= 1 while index >= 0 and tokens[index] =~ /\A[ \t]*\z/

    if index < 0
      # first character of the string
      false
    elsif tokens[index] =~ /\n[ \t]*\Z/
      # first token of the line
      false
    elsif tokens[index].group == :punct
      # preceded by a punctuation token on the same line
      i = tokens[index].rindex(/\S/)
      punc = tokens[index][i, 1]
      # true if the preceeding statement was terminating
      not NON_TERMINATING.include?(punc)
    else
      # preceded by a non punctuation token on the same line
      true
    end
  end


  OPEN_BRACKETS = {
    '{' => '}',
    '(' => ')',
    '[' => ']',
  }

  #close_bracket = {}
  #OPEN_BRACKETS.each{|open, close| opens_bracket[close] = open}
  #CLOSE_BRACKETS = opens_bracket
  # the following is more readable :)
  CLOSE_BRACKETS = {
    '}' => '{',
    ')' => '(',
    ']' => '[',
  }

  BRACKETS = CLOSE_BRACKETS.keys + OPEN_BRACKETS.keys

  OPEN_BLOCK = [
    'def',
    'class',
    'module',
    'do',
    'if',
    'unless',
    'while',
    'until',
    'begin',
    'for'
  ]

  MODIFIERS = %w[if unless while until]

  NON_TERMINATING = %w{+ - * / , . = ~ < > ( [}
end
