package me.teridax.jcash.banking.accounts;

import me.teridax.jcash.Logging;
import me.teridax.jcash.decode.StringDecoder;

import java.util.Objects;

import static me.teridax.jcash.lang.Translator.translate;

/**
 * Base class for bank accounts.
 * Stores the iban, pin and balance.
 */
@SuppressWarnings("unused")
public abstract class Account {

    /**
     * International bank account number
     */
    protected final int iban;
    /**
     * Personal identification number
     */
    protected final int pin;
    /**
     * Balance of this account
     */
    protected double balance;

    public Account(int iban, int pin, double balance) {
        this.iban = iban;
        this.pin = pin;
        this.balance = balance;
    }

    /**
     * Parses a row of a fixed amount of columns into an account.
     * This function will attempt to create an instance of two classes which inherit from Account.
     *
     * @param columns array of 6 strings
     * @return either an instance of {@link SavingsAccount} or an instance of {@link CurrentAccount}
     * @throws IllegalArgumentException if the account type cannot be determined or the provided data is invalid
     * @throws NullPointerException     if columns is null
     */
    public static Account fromColumns(String[] columns) throws IllegalArgumentException, NullPointerException {
        Objects.requireNonNull(columns);
        Logging.LOGGER.finer("Parsing account from columns");

        // deserialize fields
        var iban = StringDecoder.decodeUniqueIdentificationNumber(columns[0]);
        var pin = StringDecoder.decodeUniqueIdentificationNumber(columns[1]);
        var balance = StringDecoder.decodeCurrency(columns[2]);
        var type = StringDecoder.decodeName(columns[3]);

        // try to detect the specific runtime class to deserialize
        try {
            if (type.equals("Sparkonto")) {
                Logging.LOGGER.fine("Account detected as Sparkonto");
                var interest = StringDecoder.decodePercent(columns[4]);
                return new SavingsAccount(iban, pin, balance, interest);
            } else if (type.equals("Girokonto")) {
                Logging.LOGGER.fine("Account detected as Girokonto");
                var overdraft = StringDecoder.decodeCurrency(columns[5]);
                return new CurrentAccount(iban, pin, balance, overdraft);
            } else {
                Logging.LOGGER.severe("Account type could not be detected");
                throw new IllegalArgumentException("Invalid account type: " + type);
            }

        } catch (IllegalArgumentException | NullPointerException e) {
            Logging.LOGGER.severe("Account field could not be decoded: " + e.getMessage());
            throw new IllegalArgumentException("Account format: ", e);
        }
    }

    public int getIban() {
        return iban;
    }

    public int getPin() {
        return pin;
    }

    public double getBalance() {
        return balance;
    }

    @Override
    public int hashCode() {
        return Objects.hash(iban);
    }

    /**
     * Returns true if the parameter is an instance of class {@link Account} and both their
     * ibans are equal.
     *
     * @param obj the obj to compare to
     * @return true if the parameter is an instance of class {@link Account} and their ibans match
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Account)
            return iban == ((Account) obj).iban;

        return false;
    }

    /**
     * Returns a description of the account in form a string.
     * This method is not equal to {@link #toString()} but is intended to be
     * a method used to retrieve a formatted representation for guis.
     *
     * @return a basic description of the account in form a string
     */
    public String getDescription() {
        return String.format("%s (%s)", iban, translate(getClass().getSimpleName()));
    }

    /**
     * Add a non-negative value onto the balance of this account.
     *
     * @param amount the amount of value to add
     * @throws IllegalArgumentException if amount is negative
     */
    public void deposit(double amount) throws IllegalArgumentException {
        if (amount < 0) {
            Logging.LOGGER.severe("Cannot deposit negative amount of money: " + amount);
            throw new IllegalArgumentException("amount must be positive");
        }

        this.balance += amount;
    }

    @Override
    public String toString() {
        return String.format("@Account [iban=%d, pin=%d, balance=%.2f]", iban, pin, balance);
    }

    /**
     * Takeoff a certain amount of money from this accounts balance.
     * Saturates the result if the value to subtract is greater than the balance present.
     *
     * @param amount the amount of money to subtract from the accounts balance
     * @throws IllegalArgumentException if amount is greater than the balance present
     */
    public void takeoff(double amount) throws IllegalArgumentException {
        if (amount > this.balance) {
            Logging.LOGGER.severe("Cannot take off more money than present in balance: " + amount);
            throw new IllegalArgumentException("amount must be smaller or equals the accounts balance");
        }

        this.balance = this.balance - amount;
    }
}
