// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2012-2014 Monty Program Ab
// Copyright (c) 2015-2025 MariaDB Corporation Ab
package org.mariadb.jdbc;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.mariadb.jdbc.client.Client;
import org.mariadb.jdbc.client.impl.MultiPrimaryClient;
import org.mariadb.jdbc.client.impl.MultiPrimaryReplicaClient;
import org.mariadb.jdbc.client.impl.ReplayClient;
import org.mariadb.jdbc.client.impl.StandardClient;
import org.mariadb.jdbc.client.util.ClosableLock;
import org.mariadb.jdbc.export.HaMode;
import org.mariadb.jdbc.pool.Pools;
import org.mariadb.jdbc.util.VersionFactory;

/** MariaDB Driver */
public final class Driver implements java.sql.Driver {
  private static final Pattern identifierPattern =
      Pattern.compile("[0-9a-zA-Z$_\\u0080-\\uFFFF]*", Pattern.UNICODE_CASE);

  private static final Pattern escapePattern = Pattern.compile("[\u0000'\"\b\n\r\t\u001A\\\\]");

  private static final Map<String, String> mapper = new HashMap<>();

  static {
    try {
      DriverManager.registerDriver(new Driver());
    } catch (SQLException e) {
      // eat
    }
    mapper.put("\u0000", "\\0");
    mapper.put("'", "\\\\'");
    mapper.put("\"", "\\\\\"");
    mapper.put("\b", "\\\\b");
    mapper.put("\n", "\\\\n");
    mapper.put("\r", "\\\\r");
    mapper.put("\t", "\\\\t");
    mapper.put("\u001A", "\\\\Z");
    mapper.put("\\", "\\\\");
  }

  /**
   * Connect according to configuration
   *
   * @param configuration configuration
   * @return a Connection
   * @throws SQLException if connect fails
   */
  public static Connection connect(Configuration configuration) throws SQLException {
    ClosableLock lock = new ClosableLock();

    if (configuration.haMode() == HaMode.NONE) {
      ClientInstance<Configuration, HostAddress, ClosableLock, Boolean, Client> clientInstance =
          (configuration.transactionReplay()) ? ReplayClient::new : StandardClient::new;

      if (configuration.addresses().isEmpty())
        throw new SQLException("host, pipe or local socket must be set to connect socket");

      // loop until finding
      SQLException lastException = null;
      for (HostAddress host : configuration.addresses()) {
        try {
          Client client = clientInstance.apply(configuration, host, lock, false);
          return new Connection(configuration, lock, client);
        } catch (SQLException e) {
          lastException = e;
        }
      }
      throw lastException;
    }

    Client client =
        configuration.havePrimaryHostOnly()
            ? new MultiPrimaryClient(configuration, lock)
            : new MultiPrimaryReplicaClient(configuration, lock);
    return new Connection(configuration, lock, client);
  }

  /**
   * Connect to the given connection string.
   *
   * @param url the url to connect to
   * @return a connection
   * @throws SQLException if it is not possible to connect
   */
  public Connection connect(final String url, final Properties props) throws SQLException {
    Configuration configuration = Configuration.parse(url, props);
    if (configuration != null) {
      if (configuration.pool()) {
        return Pools.retrievePool(configuration).getPoolConnection().getConnection();
      }
      return connect(configuration);
    }
    return null;
  }

  /**
   * returns true if the driver can accept the url.
   *
   * @param url the url to test
   * @return true if the url is valid for this driver
   */
  @Override
  public boolean acceptsURL(String url) {
    return Configuration.acceptsUrl(url);
  }

  /**
   * Get the property info.
   *
   * @param url the url to get properties for
   * @param info the info props
   * @return all possible connector options
   * @throws SQLException if there is a problem getting the property info
   */
  public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
    Configuration conf = Configuration.parse(url, info);
    if (conf == null) {
      return new DriverPropertyInfo[0];
    }

    Properties propDesc = new Properties();
    try (InputStream inputStream =
        Driver.class.getClassLoader().getResourceAsStream("driver.properties")) {
      propDesc.load(inputStream);
    } catch (IOException io) {
      // eat
    }

    List<DriverPropertyInfo> props = new ArrayList<>();
    for (Field field : Configuration.Builder.class.getDeclaredFields()) {
      if (!field.getName().startsWith("_")) {
        try {
          Method getterMethod = Configuration.class.getDeclaredMethod(field.getName());
          Object obj = getterMethod.invoke(conf);
          String value = obj == null ? null : obj.toString();
          DriverPropertyInfo propertyInfo = new DriverPropertyInfo(field.getName(), value);
          propertyInfo.description = value == null ? "" : (String) propDesc.get(field.getName());
          propertyInfo.required = false;
          props.add(propertyInfo);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
          // eat error
        }
      }
    }
    return props.toArray(new DriverPropertyInfo[0]);
  }

  /**
   * gets the major version of the driver.
   *
   * @return the major versions
   */
  public int getMajorVersion() {
    return VersionFactory.getInstance().getMajorVersion();
  }

  /**
   * gets the minor version of the driver.
   *
   * @return the minor version
   */
  public int getMinorVersion() {
    return VersionFactory.getInstance().getMinorVersion();
  }

  /**
   * checks if the driver is jdbc compliant.
   *
   * @return true since the driver is not compliant
   */
  public boolean jdbcCompliant() {
    return true;
  }

  public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
    throw new SQLFeatureNotSupportedException("Use logging parameters for enabling logging.");
  }

  public static String enquoteIdentifier(String identifier, boolean alwaysQuote)
      throws SQLException {
    int len = identifier.length();

    if (isSimpleIdentifier(identifier)) {
      if (len < 1 || len > 64) {
        throw new SQLException("Invalid identifier length");
      }
      if (alwaysQuote) return "`" + identifier + "`";

      // Identifier names may begin with a numeral, but can't only contain numerals unless quoted.
      for (int i = 0; i < identifier.length(); i++) {
        if (!Character.isDigit(identifier.charAt(i))) {
          return identifier;
        }
      }
      // identifier containing only numerals must be quoted
      return "`" + identifier + "`";
    } else {
      if (identifier.contains("\u0000")) {
        throw new SQLException("Invalid name - containing u0000 character", "42000");
      }

      if (identifier.matches("^`.+`$")) {
        identifier = identifier.substring(1, identifier.length() - 1);
      }
      if (len < 1 || len > 64) {
        throw new SQLException("Invalid identifier length");
      }
      return "`" + identifier.replace("`", "``") + "`";
    }
  }

  /**
   * Enquote String value.
   *
   * @param val string value to enquote
   * @return enquoted string value
   */
  // @Override when not supporting java 8
  public static String enquoteLiteral(String val) {
    Matcher matcher = escapePattern.matcher(val);
    StringBuffer escapedVal = new StringBuffer("'");

    while (matcher.find()) {
      matcher.appendReplacement(escapedVal, mapper.get(matcher.group()));
    }
    matcher.appendTail(escapedVal);
    escapedVal.append("'");
    return escapedVal.toString();
  }

  /**
   * Retrieves whether identifier is a simple SQL identifier. The first character is an alphabetic
   * character from a through z, or from A through Z The string only contains alphanumeric
   * characters or the characters "_" and "$"
   *
   * @param identifier identifier
   * @return true if identifier doesn't have to be quoted
   * @see <a href="https://mariadb.com/kb/en/library/identifier-names/">mariadb identifier name</a>
   */
  public static boolean isSimpleIdentifier(String identifier) {
    return identifier != null
        && !identifier.isEmpty()
        && identifierPattern.matcher(identifier).matches();
  }

  @FunctionalInterface
  private interface ClientInstance<T, U, V, W, R> {
    R apply(T t, U u, V v, W w) throws SQLException;
  }
}
