/*
 * Copyright (c) 2004, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.util;

import org.postgresql.core.Encoding;

// import org.checkerframework.checker.nullness.qual.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

public class HStoreConverter {
  public static Map<String, /* @Nullable */ String> fromBytes(byte[] b, Encoding encoding)
      throws SQLException {
    Map<String, /* @Nullable */ String> m = new HashMap<String, /* @Nullable */ String>();
    int pos = 0;
    int numElements = ByteConverter.int4(b, pos);
    pos += 4;
    try {
      for (int i = 0; i < numElements; i++) {
        int keyLen = ByteConverter.int4(b, pos);
        pos += 4;
        String key = encoding.decode(b, pos, keyLen);
        pos += keyLen;
        int valLen = ByteConverter.int4(b, pos);
        pos += 4;
        String val;
        if (valLen == -1) {
          val = null;
        } else {
          val = encoding.decode(b, pos, valLen);
          pos += valLen;
        }
        m.put(key, val);
      }
    } catch (IOException ioe) {
      throw new PSQLException(
          GT.tr(
              "Invalid character data was found.  This is most likely caused by stored data containing characters that are invalid for the character set the database was created in.  The most common example of this is storing 8bit data in a SQL_ASCII database."),
          PSQLState.DATA_ERROR, ioe);
    }
    return m;
  }

  public static byte[] toBytes(Map<?, ?> m, Encoding encoding) throws SQLException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream(4 + 10 * m.size());
    byte[] lenBuf = new byte[4];
    try {
      ByteConverter.int4(lenBuf, 0, m.size());
      baos.write(lenBuf);
      for (Entry<?, ?> e : m.entrySet()) {
        Object mapKey = e.getKey();
        if (mapKey == null) {
          throw new PSQLException(GT.tr("hstore key must not be null"),
              PSQLState.INVALID_PARAMETER_VALUE);
        }
        byte[] key = encoding.encode(mapKey.toString());
        ByteConverter.int4(lenBuf, 0, key.length);
        baos.write(lenBuf);
        baos.write(key);

        if (e.getValue() == null) {
          ByteConverter.int4(lenBuf, 0, -1);
          baos.write(lenBuf);
        } else {
          byte[] val = encoding.encode(e.getValue().toString());
          ByteConverter.int4(lenBuf, 0, val.length);
          baos.write(lenBuf);
          baos.write(val);
        }
      }
    } catch (IOException ioe) {
      throw new PSQLException(
          GT.tr(
              "Invalid character data was found.  This is most likely caused by stored data containing characters that are invalid for the character set the database was created in.  The most common example of this is storing 8bit data in a SQL_ASCII database."),
          PSQLState.DATA_ERROR, ioe);
    }
    return baos.toByteArray();
  }

  public static String toString(Map<?, ?> map) {
    if (map.isEmpty()) {
      return "";
    }
    StringBuilder sb = new StringBuilder(map.size() * 8);
    for (Entry<?, ?> e : map.entrySet()) {
      appendEscaped(sb, e.getKey());
      sb.append("=>");
      appendEscaped(sb, e.getValue());
      sb.append(", ");
    }
    sb.setLength(sb.length() - 2);
    return sb.toString();
  }

  private static void appendEscaped(StringBuilder sb, /* @Nullable */ Object val) {
    if (val != null) {
      sb.append('"');
      String s = val.toString();
      for (int pos = 0; pos < s.length(); pos++) {
        char ch = s.charAt(pos);
        if (ch == '"' || ch == '\\') {
          sb.append('\\');
        }
        sb.append(ch);
      }
      sb.append('"');
    } else {
      sb.append("NULL");
    }
  }

  public static Map<String, /* @Nullable */ String> fromString(String s) {
    Map<String, /* @Nullable */ String> m = new HashMap<String, /* @Nullable */ String>();
    int pos = 0;
    StringBuilder sb = new StringBuilder();
    while (pos < s.length()) {
      sb.setLength(0);
      int start = s.indexOf('"', pos);
      int end = appendUntilQuote(sb, s, start);
      String key = sb.toString();
      pos = end + 3;

      String val;
      if (s.charAt(pos) == 'N') {
        val = null;
        pos += 4;
      } else {
        sb.setLength(0);
        end = appendUntilQuote(sb, s, pos);
        val = sb.toString();
        pos = end;
      }
      pos++;
      m.put(key, val);
    }
    return m;
  }

  private static int appendUntilQuote(StringBuilder sb, String s, int pos) {
    for (pos += 1; pos < s.length(); pos++) {
      char ch = s.charAt(pos);
      if (ch == '"') {
        break;
      }
      if (ch == '\\') {
        pos++;
        ch = s.charAt(pos);
      }
      sb.append(ch);
    }
    return pos;
  }
}
