/*
 * Copyright (C) 2000 - 2024 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception.  You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package org.silverpeas.components.mydb.model;

import org.silverpeas.components.mydb.service.MyDBRuntimeException;
import org.silverpeas.kernel.util.StringUtil;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Date;
import java.sql.JDBCType;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Objects;

/**
 * The value of a field in a table row with its type (and associated type name) in the database.
 * @author mmoquillon
 */
public class TableFieldValue implements Comparable<TableFieldValue> {

  private Object value;
  private final int type;

  /**
   * Constructs a {@link TableFieldValue} instance from the specified value represented as a
   * {@link String} object and according to the specified SQL type (a value among
   * {@link java.sql.Types}). If the specified value
   * doesn't match the SQL type of this field value, then an {@link IllegalArgumentException}
   * exception is thrown.
   * @param value the {@link String} representation of a value. Null value shouldn't be null but the
   * String "null".
   * @param sqlType the SQL type as defined in {@link java.sql.Types}.
   * @return a {@link TableFieldValue} instance.
   */
  public static TableFieldValue fromString(final String value, int sqlType) {
    TableFieldValue tableFieldValue = new TableFieldValue(null, sqlType);
    tableFieldValue.update(value);
    return tableFieldValue;
  }

  /**
   * Constructs a new value of a field in a database table.
   * @see java.sql.Types for SQL type of the value.
   * @param value the value of the field
   * @param type the SQL type of the value
   */
  TableFieldValue(final Object value, final int type) {
    this.value = value;
    this.type = type;
  }

  /**
   * Gets the code of the SQL type of this value.
   * @see java.sql.Types for the available possible codes.
   * @return the SQL type of this value as defined in {@link java.sql.Types}.
   */
  public int getType() {
    return type;
  }

  /**
   * Is this value a text?
   * @return true if the type of this value is a text, false otherwise.
   */
  public boolean isText() {
    return SqlTypes.isText(this.type);
  }

  /**
   * Is this value an empty text?
   * @return true if this value is a text and it is empty. False otherwise.
   */
  public boolean isEmpty() {
    return isText() && toString().isEmpty();
  }

  /**
   * Updates this value with the textual representation of the new value. If the specified value
   * doesn't match the SQL type of this field value, then an {@link IllegalArgumentException}
   * exception is thrown.
   * @param value a {@link String} representation of the value. Null value shouldn't be null but the
   * String "null".
   */
  public void update(final String value) {
    if (value == null || value.equals("null")) {
      this.value = null;
    } else if (SqlTypes.isText(this.type)) {
      this.value = value;
    } else if (SqlTypes.isDate(this.type)) {
      try {
        this.value = Date.valueOf(value);
      } catch (IllegalArgumentException e) {
        throw new IllegalArgumentException("The date '" + value +
            "' is not in the database date escape format (yyyy-[m]m-[d]d)");
      }
    } else if (SqlTypes.isTime(this.type)) {
      this.value = Time.valueOf(value);
    } else if (SqlTypes.isTimestamp(this.type)) {
      this.value = Timestamp.valueOf(value);
    } else if (SqlTypes.isBoolean(this.type)) {
      this.value = StringUtil.getBooleanValue(value);
    } else if (SqlTypes.isBigInteger(this.type)) {
      this.value = BigInteger.valueOf(Long.parseLong(value));
    } else if (SqlTypes.isDecimal(this.type)) {
      this.value = BigDecimal.valueOf(Double.parseDouble(value));
    } else if (SqlTypes.isInteger(this.type)) {
      this.value = Integer.valueOf(value);
    } else if (SqlTypes.isFloat(this.type)) {
      this.value = Float.valueOf(value);
    } else if (SqlTypes.isDouble(this.type)) {
      this.value = Double.valueOf(value);
    }
  }

  @Override
  public boolean equals(final Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    final TableFieldValue that = (TableFieldValue) o;
    return Objects.equals(value, that.value) && Objects.equals(type, that.type);
  }

  @Override
  public int hashCode() {
    return Objects.hash(value, type);
  }

  /**
   * Compares this {@link TableFieldValue} with the specified one. The comparing is actually done
   * on the wrapped values themselves. If the wrapped values satisfy the {@link Comparable}
   * interface then the {@link Comparable#compareTo(Object)} method is used, otherwise both of them
   * are converted in {@link String} objects and these {@link String} instances are then compared
   * between themselves.
   * @param o another {@link TableFieldValue} with which this one is compared.
   * @return the comparing distance between the two {@link TableFieldValue} instances.
   */
  @Override
  public int compareTo(final TableFieldValue o) {
    if (this.type != o.type) {
      final String typeName = JDBCType.valueOf(this.type).getName();
      throw new MyDBRuntimeException(
          "The two table field values aren't of the same type: this is of type " + typeName +
              " whereas other if of type " + typeName);
    }
    final int compare;
    if (o.value == null && this.value == null) {
      compare = 0;
    } else if (o.value != null && this.value instanceof Comparable) {
      //noinspection unchecked,rawtypes
      compare = ((Comparable) this.value).compareTo(o.value);
    } else if (o.value == null) {
      return 1;
    } else if (this.value == null) {
      return -1;
    } else {
      compare = toString().compareTo(o.toString());
    }
    return compare;
  }

  /**
   * Converts this table field value to an SQL object as defined by its SQL type.
   * @return this value as an SQL object.
   */
  Object toSQLObject() {
    return value;
  }

  @Override
  public String toString() {
    return value == null ? "null" : value.toString();
  }

  public TableFieldValue getCopy() {
    return new TableFieldValue(value, type);
  }
}
