/* The Mia! Accounting Flask Project
 * journal-entry-editor.js: The JavaScript for the journal entry editor
 */

/*  Copyright (c) 2023 imacat.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/* Author: imacat@mail.imacat.idv.tw (imacat)
 * First written: 2023/2/25
 */
"use strict";

/**
 * The journal entry editor.
 *
 */
class JournalEntryEditor {

    /**
     * The transaction form
     * @type {TransactionForm}
     */
    form;

    /**
     * The journal entry editor
     * @type {HTMLFormElement}
     */
    #element;

    /**
     * The bootstrap modal
     * @type {HTMLDivElement}
     */
    #modal;

    /**
     * The entry type, either "debit" or "credit"
     * @type {string}
     */
    entryType;

    /**
     * The prefix of the HTML ID and class
     * @type {string}
     */
    #prefix = "accounting-entry-editor"

    /**
     * The container of the original entry
     * @type {HTMLDivElement}
     */
    #originalEntryContainer;

    /**
     * The control of the original entry
     * @type {HTMLDivElement}
     */
    #originalEntryControl;

    /**
     * The original entry
     * @type {HTMLDivElement}
     */
    #originalEntry;

    /**
     * The error message of the original entry
     * @type {HTMLDivElement}
     */
    #originalEntryError;

    /**
     * The delete button of the original entry
     * @type {HTMLButtonElement}
     */
    #originalEntryDelete;

    /**
     * The control of the summary
     * @type {HTMLDivElement}
     */
    #summaryControl;

    /**
     * The summary
     * @type {HTMLDivElement}
     */
    #summary;

    /**
     * The error message of the summary
     * @type {HTMLDivElement}
     */
    #summaryError;

    /**
     * The control of the account
     * @type {HTMLDivElement}
     */
    #accountControl;

    /**
     * The account
     * @type {HTMLDivElement}
     */
    #account;

    /**
     * The error message of the account
     * @type {HTMLDivElement}
     */
    #accountError;

    /**
     * The amount
     * @type {HTMLInputElement}
     */
    #amount;

    /**
     * The error message of the amount
     * @type {HTMLDivElement}
     */
    #amountError;

    /**
     * The journal entry to edit
     * @type {JournalEntrySubForm|null}
     */
    entry;

    /**
     * The debit or credit entry side sub-form
     * @type {DebitCreditSideSubForm}
     */
    #side;

    /**
     * Whether the journal entry needs offset
     * @type {boolean}
     */
    isNeedOffset = false;

    /**
     * The ID of the original entry
     * @type {string|null}
     */
    originalEntryId = null;

    /**
     * The date of the original entry
     * @type {string|null}
     */
    originalEntryDate = null;

    /**
     * The text of the original entry
     * @type {string|null}
     */
    originalEntryText = null;

    /**
     * The account code
     * @type {string|null}
     */
    accountCode = null;

    /**
     * The account text
     * @type {string|null}
     */
    accountText = null;

    /**
     * The summary
     * @type {string|null}
     */
    summary = null;

    /**
     * The amount
     * @type {string}
     */
    amount = "";

    /**
     * The summary editors
     * @type {{debit: SummaryEditor, credit: SummaryEditor}}
     */
    #summaryEditors = {};

    /**
     * The account selectors
     * @type {{debit: AccountSelector, credit: AccountSelector}}
     */
    #accountSelectors = {};

    /**
     * The original entry selector
     * @type {OriginalEntrySelector}
     */
    originalEntrySelector;

    /**
     * Constructs a new journal entry editor.
     *
     * @param form {TransactionForm} the transaction form
     */
    constructor(form) {
        this.form = form;
        this.#element = document.getElementById(this.#prefix);
        this.#modal = document.getElementById(this.#prefix + "-modal");
        this.#originalEntryContainer = document.getElementById(this.#prefix + "-original-entry-container");
        this.#originalEntryControl = document.getElementById(this.#prefix + "-original-entry-control");
        this.#originalEntry = document.getElementById(this.#prefix + "-original-entry");
        this.#originalEntryError = document.getElementById(this.#prefix + "-original-entry-error");
        this.#originalEntryDelete = document.getElementById(this.#prefix + "-original-entry-delete");
        this.#summaryControl = document.getElementById(this.#prefix + "-summary-control");
        this.#summary = document.getElementById(this.#prefix + "-summary");
        this.#summaryError = document.getElementById(this.#prefix + "-summary-error");
        this.#accountControl = document.getElementById(this.#prefix + "-account-control");
        this.#account = document.getElementById(this.#prefix + "-account");
        this.#accountError = document.getElementById(this.#prefix + "-account-error")
        this.#amount = document.getElementById(this.#prefix + "-amount");
        this.#amountError = document.getElementById(this.#prefix + "-amount-error");
        for (const entryType of ["debit", "credit"]) {
            this.#summaryEditors[entryType] = new SummaryEditor(this, entryType);
        }
        for (const entryType of ["debit", "credit"]) {
            this.#accountSelectors[entryType] = new AccountSelector(this, entryType);
        }
        this.originalEntrySelector = new OriginalEntrySelector();
        this.#originalEntryControl.onclick = () => this.originalEntrySelector.onOpen(this, this.originalEntryId)
        this.#originalEntryDelete.onclick = () => this.clearOriginalEntry();
        this.#summaryControl.onclick = () => this.#summaryEditors[this.entryType].onOpen();
        this.#accountControl.onclick = () => this.#accountSelectors[this.entryType].onOpen();
        this.#amount.onchange = () => this.#validateAmount();
        this.#element.onsubmit = () => {
            if (this.#validate()) {
                if (this.entry === null) {
                    this.entry = this.#side.addJournalEntry();
                }
                this.amount = this.#amount.value;
                this.entry.save(this);
                bootstrap.Modal.getInstance(this.#modal).hide();
            }
            return false;
        };
    }

    /**
     * Saves the original entry from the original entry selector.
     *
     * @param originalEntry {OriginalEntry} the original entry
     */
    saveOriginalEntry(originalEntry) {
        this.isNeedOffset = false;
        this.#originalEntryContainer.classList.remove("d-none");
        this.#originalEntryControl.classList.add("accounting-not-empty");
        this.originalEntryId = originalEntry.id;
        this.originalEntryDate = originalEntry.date;
        this.originalEntryText = originalEntry.text;
        this.#originalEntry.innerText = originalEntry.text;
        this.#setEnableSummaryAccount(false);
        if (originalEntry.summary === "") {
            this.#summaryControl.classList.remove("accounting-not-empty");
        } else {
            this.#summaryControl.classList.add("accounting-not-empty");
        }
        this.summary = originalEntry.summary === ""? null: originalEntry.summary;
        this.#summary.innerText = originalEntry.summary;
        this.#accountControl.classList.add("accounting-not-empty");
        this.accountCode = originalEntry.accountCode;
        this.accountText = originalEntry.accountText;
        this.#account.innerText = originalEntry.accountText;
        this.#amount.value = String(originalEntry.netBalance);
        this.#amount.max = String(originalEntry.netBalance);
        this.#amount.min = "0";
        this.#validate();
    }

    /**
     * Clears the original entry.
     *
     */
    clearOriginalEntry() {
        this.isNeedOffset = false;
        this.#originalEntryContainer.classList.add("d-none");
        this.#originalEntryControl.classList.remove("accounting-not-empty");
        this.originalEntryId = null;
        this.originalEntryDate = null;
        this.originalEntryText = null;
        this.#originalEntry.innerText = "";
        this.#setEnableSummaryAccount(true);
        this.#accountControl.classList.remove("accounting-not-empty");
        this.accountCode = null;
        this.accountText = null;
        this.#account.innerText = "";
        this.#amount.max = "";
    }

    /**
     * Returns the currency code.
     *
     * @return {string} the currency code
     */
    getCurrencyCode() {
        return this.#side.currency.getCurrencyCode();
    }

    /**
     * Saves the summary from the summary editor.
     *
     * @param summary {string} the summary
     */
    saveSummary(summary) {
        if (summary === "") {
            this.#summaryControl.classList.remove("accounting-not-empty");
        } else {
            this.#summaryControl.classList.add("accounting-not-empty");
        }
        this.summary = summary === ""? null: summary;
        this.#summary.innerText = summary;
        this.#validateSummary();
        bootstrap.Modal.getOrCreateInstance(this.#modal).show();
    }

    /**
     * Saves the summary with the suggested account from the summary editor.
     *
     * @param summary {string} the summary
     * @param accountCode {string} the account code
     * @param accountText {string} the account text
     * @param isAccountNeedOffset {boolean} true if the journal entries in the account need offset, or false otherwise
     */
    saveSummaryWithAccount(summary, accountCode, accountText, isAccountNeedOffset) {
        this.isNeedOffset = isAccountNeedOffset;
        this.#accountControl.classList.add("accounting-not-empty");
        this.accountCode = accountCode;
        this.accountText = accountText;
        this.#account.innerText = accountText;
        this.#validateAccount();
        this.saveSummary(summary)
    }

    /**
     * Clears the account.
     *
     */
    clearAccount() {
        this.isNeedOffset = false;
        this.#accountControl.classList.remove("accounting-not-empty");
        this.accountCode = null;
        this.accountText = null;
        this.#account.innerText = "";
        this.#validateAccount();
    }

    /**
     * Sets the account.
     *
     * @param code {string} the account code
     * @param text {string} the account text
     * @param isOffsetNeeded {boolean} true if the journal entries in the account need offset or false otherwise
     */
    saveAccount(code, text, isOffsetNeeded) {
        this.isNeedOffset = isOffsetNeeded;
        this.#accountControl.classList.add("accounting-not-empty");
        this.accountCode = code;
        this.accountText = text;
        this.#account.innerText = text;
        this.#validateAccount();
    }

    /**
     * Validates the form.
     *
     * @returns {boolean} true if valid, or false otherwise
     */
    #validate() {
        let isValid = true;
        isValid = this.#validateOriginalEntry() && isValid;
        isValid = this.#validateSummary() && isValid;
        isValid = this.#validateAccount() && isValid;
        isValid = this.#validateAmount() && isValid
        return isValid;
    }

    /**
     * Validates the original entry.
     *
     * @return {boolean} true if valid, or false otherwise
     * @private
     */
    #validateOriginalEntry() {
        this.#originalEntryControl.classList.remove("is-invalid");
        this.#originalEntryError.innerText = "";
        return true;
    }

    /**
     * Validates the summary.
     *
     * @return {boolean} true if valid, or false otherwise
     * @private
     */
    #validateSummary() {
        this.#summary.classList.remove("is-invalid");
        this.#summaryError.innerText = "";
        return true;
    }

    /**
     * Validates the account.
     *
     * @return {boolean} true if valid, or false otherwise
     */
    #validateAccount() {
        if (this.accountCode === null) {
            this.#accountControl.classList.add("is-invalid");
            this.#accountError.innerText = A_("Please select the account.");
            return false;
        }
        this.#accountControl.classList.remove("is-invalid");
        this.#accountError.innerText = "";
        return true;
    }

    /**
     * Validates the amount.
     *
     * @return {boolean} true if valid, or false otherwise
     * @private
     */
    #validateAmount() {
        this.#amount.value = this.#amount.value.trim();
        this.#amount.classList.remove("is-invalid");
        if (this.#amount.value === "") {
            this.#amount.classList.add("is-invalid");
            this.#amountError.innerText = A_("Please fill in the amount.");
            return false;
        }
        const amount =new Decimal(this.#amount.value);
        if (amount.lessThanOrEqualTo(0)) {
            this.#amount.classList.add("is-invalid");
            this.#amountError.innerText = A_("Please fill in a positive amount.");
            return false;
        }
        if (this.#amount.max !== "") {
            if (amount.greaterThan(new Decimal(this.#amount.max))) {
                this.#amount.classList.add("is-invalid");
                this.#amountError.innerText = A_("The amount must not exceed the net balance %(balance)s of the original entry.", {balance: new Decimal(this.#amount.max)});
                return false;
            }
        }
        if (this.#amount.min !== "") {
            const min = new Decimal(this.#amount.min);
            if (amount.lessThan(min)) {
                this.#amount.classList.add("is-invalid");
                this.#amountError.innerText = A_("The amount must not be less than the offset total %(total)s.", {total: formatDecimal(min)});
                return false;
            }
        }
        this.#amount.classList.remove("is-invalid");
        this.#amountError.innerText = "";
        return true;
    }

    /**
     * The callback when adding a new journal entry.
     *
     * @param side {DebitCreditSideSubForm} the debit or credit side sub-form
     */
    onAddNew(side) {
        this.entry = null;
        this.#side = side;
        this.entryType = this.#side.entryType;
        this.isNeedOffset = false;
        this.#originalEntryContainer.classList.add("d-none");
        this.#originalEntryControl.classList.remove("accounting-not-empty");
        this.#originalEntryControl.classList.remove("is-invalid");
        this.originalEntryId = null;
        this.originalEntryDate = null;
        this.originalEntryText = null;
        this.#originalEntry.innerText = "";
        this.#setEnableSummaryAccount(true);
        this.#summaryControl.classList.remove("accounting-not-empty");
        this.#summaryControl.classList.remove("is-invalid");
        this.summary = null;
        this.#summary.innerText = ""
        this.#summaryError.innerText = ""
        this.#accountControl.classList.remove("accounting-not-empty");
        this.#accountControl.classList.remove("is-invalid");
        this.accountCode = null;
        this.accountText = null;
        this.#account.innerText = "";
        this.#accountError.innerText = "";
        this.#amount.value = "";
        this.#amount.max = "";
        this.#amount.min = "0";
        this.#amount.classList.remove("is-invalid");
        this.#amountError.innerText = "";
    }

    /**
     * The callback when editing a journal entry.
     *
     * @param entry {JournalEntrySubForm} the journal entry sub-form
     * @param originalEntryId {string} the ID of the original entry
     * @param originalEntryDate {string} the date of the original entry
     * @param originalEntryText {string} the text of the original entry
     * @param summary {string} the summary
     * @param accountCode {string} the account code
     * @param accountText {string} the account text
     * @param amount {string} the amount
     * @param amountMin {string} the minimal amount
     */
    onEdit(entry, originalEntryId, originalEntryDate, originalEntryText, summary, accountCode, accountText, amount, amountMin) {
        this.entry = entry;
        this.#side = entry.side;
        this.entryType = this.#side.entryType;
        this.isNeedOffset = entry.isNeedOffset();
        if (originalEntryId === "") {
            this.#originalEntryContainer.classList.add("d-none");
            this.#originalEntryControl.classList.remove("accounting-not-empty");
        } else {
            this.#originalEntryContainer.classList.remove("d-none");
            this.#originalEntryControl.classList.add("accounting-not-empty");
        }
        this.originalEntryId = originalEntryId === ""? null: originalEntryId;
        this.originalEntryDate = originalEntryDate === ""? null: originalEntryDate;
        this.originalEntryText = originalEntryText === ""? null: originalEntryText;
        this.#originalEntry.innerText = originalEntryText;
        this.#setEnableSummaryAccount(!entry.isMatched && originalEntryId === "");
        if (summary === "") {
            this.#summaryControl.classList.remove("accounting-not-empty");
        } else {
            this.#summaryControl.classList.add("accounting-not-empty");
        }
        this.summary = summary === ""? null: summary;
        this.#summary.innerText = summary;
        if (accountCode === "") {
            this.#accountControl.classList.remove("accounting-not-empty");
        } else {
            this.#accountControl.classList.add("accounting-not-empty");
        }
        this.accountCode = accountCode;
        this.accountText = accountText;
        this.#account.innerText = accountText;
        this.#amount.value = amount;
        const maxAmount = this.#getMaxAmount();
        this.#amount.max = maxAmount === null? "": maxAmount;
        this.#amount.min = amountMin;
        this.#validate();
    }

    /**
     * Finds out the max amount.
     *
     * @return {Decimal|null} the max amount
     */
    #getMaxAmount() {
        if (this.originalEntryId === null) {
            return null;
        }
        return this.originalEntrySelector.getNetBalance(this.entry, this.form, this.originalEntryId);
    }

    /**
     * Sets the enable status of the summary and account.
     *
     * @param isEnabled {boolean} true to enable, or false otherwise
     */
    #setEnableSummaryAccount(isEnabled) {
        if (isEnabled) {
            this.#summaryControl.dataset.bsToggle = "modal";
            this.#summaryControl.dataset.bsTarget = "#accounting-summary-editor-" + this.#side.entryType + "-modal";
            this.#summaryControl.classList.remove("accounting-disabled");
            this.#summaryControl.classList.add("accounting-clickable");
            this.#accountControl.dataset.bsToggle = "modal";
            this.#accountControl.dataset.bsTarget = "#accounting-account-selector-" + this.#side.entryType + "-modal";
            this.#accountControl.classList.remove("accounting-disabled");
            this.#accountControl.classList.add("accounting-clickable");
        } else {
            this.#summaryControl.dataset.bsToggle = "";
            this.#summaryControl.dataset.bsTarget = "";
            this.#summaryControl.classList.add("accounting-disabled");
            this.#summaryControl.classList.remove("accounting-clickable");
            this.#accountControl.dataset.bsToggle = "";
            this.#accountControl.dataset.bsTarget = "";
            this.#accountControl.classList.add("accounting-disabled");
            this.#accountControl.classList.remove("accounting-clickable");
        }
    }
}