module PG
  module EscapeHelper
    extend self

    # `#escape_identifier` escapes a string for use as an SQL identifier, such
    # as a table, column, or function name. This is useful when a user-supplied
    # identifier might contain special characters that would otherwise not be
    # interpreted as part of the identifier by the SQL parser, or when the
    # identifier might contain upper case characters whose case should be
    # preserved.
    def escape_identifier(str)
      escape str, true
    end

    # `#escape_literal` escapes a string for use within an SQL command. This is
    # useful when inserting data values as literal constants in SQL commands.
    # Certain characters (such as quotes and backslashes) must be escaped to
    # prevent them from being interpreted specially by the SQL parser.
    # PQescapeLiteral performs this operation.
    #
    # Note that it is not necessary nor correct to do escaping when a data
    # value is passed as a separate parameter in `#exec`
    def escape_literal(str)
      escape str, false
    end

    # `#escape_literal` escapes binary data suitable for use with the BYTEA type.
    def escape_literal(slice : Slice(UInt8))
      ssize = slice.size * 2 + 4
      String.new(ssize) do |buffer|
        buffer[0] = '\''.ord.to_u8
        buffer[1] = '\\'.ord.to_u8
        buffer[2] = 'x'.ord.to_u8
        slice.hexstring(buffer + 3)
        buffer[ssize - 1] = '\''.ord.to_u8
        {ssize, ssize}
      end
    end

    # reimplimentation of PQescapeInternal
    # todo this should take into account server encoding if not utf8
    private def escape(str : String, as_ident : Bool)
      num_quotes = 0
      num_backslashes = 0
      quote_char = as_ident ? '"' : '\''

      # scan the string for characters that must be escaped
      str.each_char do |char|
        case char
        when '\\'
          num_backslashes += 1
        when quote_char
          num_quotes += 1
        else
          # skip
        end
      end

      literal_with_backslashes = (!as_ident && num_backslashes > 0)

      result_size = str.size + num_quotes + 2
      if literal_with_backslashes
        result_size += num_backslashes + 2
      end

      String.build(result_size) do |build|
        if literal_with_backslashes
          build << ' ' << 'E'
        end
        build << quote_char

        if num_backslashes == num_quotes == 0
          str.each_char { |c| build << c }
        else
          str.each_char do |c|
            case c
            when quote_char
              build << quote_char
            when '\\'
              build << '\\' unless as_ident
            else
              # ignore
            end
            build << c
          end
        end

        build << quote_char
      end
    end
  end

  class Connection
    include EscapeHelper
  end
end
