# Copyright 2021 Vincent Texier <vit@free.fr>
#
# This software is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This software 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import logging
import sys
from collections import OrderedDict
from typing import List, Optional

import qrcode
from PyQt5.QtCore import QEvent, QLocale, QPoint, QTimer
from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import QApplication, QMainWindow, QSizePolicy, QWidget

from tikka.domains.application import Application
from tikka.domains.entities.account import Account, AccountCryptoType
from tikka.domains.entities.address import DisplayAddress
from tikka.domains.entities.constants import AMOUNT_UNIT_KEY, TEST_DATA_PATH
from tikka.domains.entities.identity import Identity, IdentityStatus
from tikka.domains.entities.profile import Profile
from tikka.domains.entities.smith import Smith, SmithStatus
from tikka.slots.pyqt.entities.constants import (
    ACCOUNT_TRANSFER_HISTORY_LIMIT,
    ADDRESS_MONOSPACE_FONT_NAME,
    ICON_ACCOUNT_NO_WALLET,
    ICON_ACCOUNT_WALLET_LOCKED,
    ICON_ACCOUNT_WALLET_UNLOCKED,
    ICON_IDENTITY,
    ICON_IDENTITY_MEMBER,
    ICON_IDENTITY_MEMBER_NOT_VALIDATED,
    ICON_IDENTITY_NOT_MEMBER,
    SELECTED_UNIT_PREFERENCES_KEY,
)
from tikka.slots.pyqt.entities.event_bus import EventBus
from tikka.slots.pyqt.entities.qrcode_image import QRCodeImage
from tikka.slots.pyqt.entities.worker import ThreadManager
from tikka.slots.pyqt.resources.gui.widgets.account_rc import Ui_AccountWidget
from tikka.slots.pyqt.widgets.account_menu import AccountPopupMenu
from tikka.slots.pyqt.widgets.account_tabs.identity import AccountIdentityWidget
from tikka.slots.pyqt.widgets.account_tabs.profile import AccountProfileWidget
from tikka.slots.pyqt.widgets.account_tabs.smith import AccountSmithWidget
from tikka.slots.pyqt.widgets.account_tabs.technical_committee import (
    AccountTechnicalCommitteeWidget,
)
from tikka.slots.pyqt.widgets.account_tabs.transfers import TransfersWidget
from tikka.slots.pyqt.windows.transfer import TransferWindow


class AccountWidget(QWidget, Ui_AccountWidget):
    """
    AccountWidget class
    """

    TRANSFERS_TAB_INDEX = 0
    IDENTITY_TAB_INDEX = 1
    SMITH_TAB_INDEX = 2
    TECHNICAL_COMMITTEE_TAB_INDEX = 3
    PROFILE_TAB_INDEX = 4

    def __init__(
        self,
        application: Application,
        account: Account,
        parent: Optional[QWidget] = None,
    ) -> None:
        """
        Init AccountWidget instance

        :param application: Application instance
        :param account: Account instance
        :param parent: QWidget instance
        """
        super().__init__(parent=parent)
        self.setupUi(self)

        self.application = application
        self._ = self.application.translator.gettext

        self.event_bus = EventBus.instance()
        self.event_bus.repository_data_updated.connect(
            self.repository_data_updated_event_bus
        )
        self.event_bus.account_updated.connect(self.on_account_updated_event_bus)
        self.event_bus.account_renamed.connect(self.on_account_renamed_event_bus)
        self.event_bus.account_wallet_updated.connect(
            self.update_ui_per_account_on_event_bus
        )
        self.event_bus.account_balances_updated.connect(
            self.on_account_balances_updated
        )
        self.event_bus.identities_updated.connect(self.on_identities_updated_event_bus)
        self.event_bus.account_identities_updated.connect(
            self.on_account_identities_updated_event_bus
        )
        self.event_bus.smith_updated.connect(self.update_ui_per_account_on_event_bus)

        self.event_bus.account_profiles_updated.connect(
            self.on_account_profiles_updated_event_bus
        )
        self.event_bus.node_connected.connect(self.on_node_connection_changed_event_bus)
        self.event_bus.node_disconnected.connect(
            self.on_node_connection_changed_event_bus
        )
        self.event_bus.currency_unit_changed.connect(
            self.on_currency_unit_changed_event_bus
        )

        self.account: Account = account
        self.identity: Optional[Identity] = self.application.identities.get_by_address(
            self.account.address
        )
        self.smith: Optional[Smith] = (
            self.application.smiths.get(self.identity.index)
            if self.identity is not None
            else None
        )

        self.monospace_font = QFont(ADDRESS_MONOSPACE_FONT_NAME)
        self.monospace_font.setStyleHint(QFont.Monospace)
        self.addressValueLabel.setFont(self.monospace_font)
        self.addressValueLabel.setText(self.account.address)
        if self.account.legacy_v1:
            self.v1AddressLabel.show()
            self.v1AddressValueLabel.setText(
                self.account.get_v1_address(
                    self.application.currencies.get_current().ss58_format, True
                )
            )
        else:
            self.v1AddressLabel.hide()

        # creating a pix map of qr code
        qr_code_pixmap = qrcode.make(
            self.account.address, image_factory=QRCodeImage
        ).pixmap()

        # set qrcode to the label
        self.QRCodeAddressLabel.setPixmap(qr_code_pixmap)

        # static transfers tab
        self.history_limit = ACCOUNT_TRANSFER_HISTORY_LIMIT
        self.history_page_size = 100
        self.history_scroll_offset = 0
        self.transfers_widget: TransfersWidget = TransfersWidget(
            self.application, self.account, self
        )
        self.tabWidgetPage1.layout().addWidget(self.transfers_widget)
        self.transfers_widget.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Expanding
        )

        # identity tab
        self.identity_widget: Optional[AccountIdentityWidget] = None

        # smith tab
        self.smith_widget: Optional[AccountSmithWidget] = None

        # technical committee tab
        self.technical_committee_widget: Optional[
            AccountTechnicalCommitteeWidget
        ] = None

        # profile tab
        self.profile_widget: Optional[AccountProfileWidget] = None

        ##############################
        # ASYNC METHODS
        ##############################
        self.thread_manager: ThreadManager = ThreadManager(self)

        # events
        self.refreshButton.clicked.connect(self._on_refresh_button_clicked)
        self.transferToButton.clicked.connect(self.on_transfer_to_button_clicked)
        self.transferFromButton.clicked.connect(self.on_transfer_from_button_clicked)
        self.customContextMenuRequested.connect(self.on_account_context_menu)

        self._update_ui()

    def on_transfer_to_button_clicked(self):
        """
        When user click on transfer to button

        :return:
        """
        TransferWindow(
            self.application, None, self.account, parent=self.parent()
        ).exec_()

    def on_transfer_from_button_clicked(self):
        """
        When user click on transfer from button

        :return:
        """
        TransferWindow(
            self.application, self.account, None, parent=self.parent()
        ).exec_()

    def _on_refresh_button_clicked(self, _: QEvent):
        """
        Triggered when user click on Refresh button

        :param _: QEvent instance
        :return:
        """
        self.errorLabel.setText("")
        if self.application.connections.node.is_connected():
            self.thread_manager.start(
                self.fetch_account_from_network,
                self._on_finished_fetch_account_from_network,
            )

            if self.smith_widget is not None:
                try:
                    self.smith_widget.async_refresh_data_from_network()
                except Exception as exception:
                    self.errorLabel.setText(self._(str(exception)))
                    logging.exception(exception)

            if self.technical_committee_widget is not None:
                try:
                    self.technical_committee_widget.async_refresh_data_from_network()
                except Exception as exception:
                    self.errorLabel.setText(self._(str(exception)))
                    logging.exception(exception)

        if self.application.connections.indexer.is_connected():
            try:
                self.transfers_widget.async_refresh_data_from_network()
            except Exception as exception:
                self.errorLabel.setText(self._(str(exception)))
                logging.exception(exception)

            if self.identity_widget is not None:
                try:
                    self.identity_widget.async_refresh_data_from_network()
                except Exception as exception:
                    self.errorLabel.setText(self._(str(exception)))
                    logging.exception(exception)

        if self.application.connections.datapod.is_connected():
            if self.profile_widget is not None:
                try:
                    self.profile_widget.async_refresh_data_from_network()
                except Exception as exception:
                    self.errorLabel.setText(self._(str(exception)))
                    logging.exception(exception)

    def fetch_account_from_network(self):
        """
        Get last account data from the network

        :return:
        """
        self.refreshButton.setEnabled(False)

        try:
            self.application.accounts.network_update_account(self.account)
        except Exception as exception:
            self.errorLabel.setText(self._(str(exception)))
            logging.exception(exception)
        else:
            try:
                identity = self.application.identities.network_update_identity(
                    self.account.address
                )
            except Exception as exception:
                self.errorLabel.setText(self._(str(exception)))
                logging.exception(exception)
            else:
                if identity is not None:
                    try:
                        self.application.smiths.network_update_smith(identity.index)
                    except Exception as exception:
                        self.errorLabel.setText(self._(str(exception)))
                        logging.exception(exception)

    def _on_finished_fetch_account_from_network(self):
        """
        Triggered when async request fetch_account_from_network is finished

        :return:
        """
        self._update_ui()
        self.refreshButton.setEnabled(True)
        logging.debug("Account widget update %s", self.account.address)

    def _update_ui(self):
        """
        Update UI from self.account

        :return:
        """
        # update local data from DB
        self.account = self.application.accounts.get_by_address(self.account.address)
        self.identity = self.application.identities.get_by_address(self.account.address)
        self.smith = (
            self.application.smiths.get(self.identity.index)
            if self.identity is not None
            else None
        )
        self.profile = self.application.profiles.get(self.account.address)

        unit_preference = self.application.repository.preferences.get(
            SELECTED_UNIT_PREFERENCES_KEY
        )
        if unit_preference is not None:
            amount = self.application.amounts.get_amount(unit_preference)
        else:
            amount = self.application.amounts.get_amount(AMOUNT_UNIT_KEY)

        if self.account.balance is None:
            self.balanceValueLabel.setText("?")
            self.balanceReservedValueLabel.setText("")
            self.transferFromButton.setEnabled(False)
        else:
            self.balanceValueLabel.setText(
                self.locale().toCurrencyString(
                    amount.value(self.account.balance), amount.symbol()
                )
            )
            display_reserved_balance = self.locale().toCurrencyString(
                amount.value(self.account.balance_reserved), amount.symbol()
            )
            self.balanceReservedValueLabel.setText(f"[-{display_reserved_balance}]")
            self.transferFromButton.setEnabled(True)

        if self.application.wallets.exists(self.account.address):
            if self.account.balance is None or self.account.balance == 0:
                self.transferFromButton.setEnabled(False)
            else:
                self.transferFromButton.setEnabled(True)
            if self.application.wallets.is_unlocked(self.account.address):
                self.lockStatusIcon.setPixmap(QPixmap(ICON_ACCOUNT_WALLET_UNLOCKED))
            else:
                self.lockStatusIcon.setPixmap(QPixmap(ICON_ACCOUNT_WALLET_LOCKED))
        else:
            self.lockStatusIcon.setPixmap(QPixmap(ICON_ACCOUNT_NO_WALLET))
            self.transferFromButton.setEnabled(False)

        # identity infos
        if self.identity is not None:
            display_identity_status = {
                IdentityStatus.UNCONFIRMED.value: self._("Unconfirmed"),
                IdentityStatus.UNVALIDATED.value: self._("Unvalidated"),
                IdentityStatus.MEMBER.value: self._("Member"),
                IdentityStatus.NOT_MEMBER.value: self._("Not member"),
                IdentityStatus.REVOKED.value: self._("Revoked"),
            }

            identity_status_color = {
                IdentityStatus.UNCONFIRMED.value: "orange",
                IdentityStatus.UNVALIDATED.value: "orange",
                IdentityStatus.MEMBER.value: "green",
                IdentityStatus.NOT_MEMBER.value: "red",
                IdentityStatus.REVOKED.value: "red",
            }
            identity_status_style = f"""
                                    color: white;
                                    border: 3px solid "{identity_status_color[self.identity.status.value]}";
                                    border-radius: 10%;
                                    background: "{identity_status_color[self.identity.status.value]}";
                                    """

            display_identity_icon = {
                IdentityStatus.UNCONFIRMED.value: QPixmap(
                    ICON_IDENTITY_MEMBER_NOT_VALIDATED
                ),
                IdentityStatus.UNVALIDATED.value: QPixmap(
                    ICON_IDENTITY_MEMBER_NOT_VALIDATED
                ),
                IdentityStatus.MEMBER.value: QPixmap(ICON_IDENTITY_MEMBER),
                IdentityStatus.NOT_MEMBER.value: QPixmap(ICON_IDENTITY_NOT_MEMBER),
                IdentityStatus.REVOKED.value: QPixmap(ICON_IDENTITY_NOT_MEMBER),
            }

            self.identityStatusIconLabel.setPixmap(
                display_identity_icon[self.identity.status.value]
            )
            identity_name = self.identity.name or ""
            self.identityNameValueLabel.setText(
                f"{identity_name}#{self.identity.index}"
            )
            self.identityValueLabel.setText(
                display_identity_status[self.identity.status.value]
            )
            self.identityValueLabel.setStyleSheet(identity_status_style)

            # identity tab
            if self.identity_widget is not None:
                self.identity_widget.update_ui()
            else:
                self.identity_widget = AccountIdentityWidget(
                    self.application, self.account, self
                )
                self.tabWidgetPage2.layout().addWidget(self.identity_widget)
                self.identity_widget.setSizePolicy(
                    QSizePolicy.Expanding, QSizePolicy.Expanding
                )
                self.accountTabWidget.setTabVisible(self.IDENTITY_TAB_INDEX, True)

            if self.smith is not None:
                display_smith_status = {
                    SmithStatus.INVITED.value: self._("Invited"),
                    SmithStatus.PENDING.value: self._("Pending"),
                    SmithStatus.SMITH.value: self._("Smith"),
                    SmithStatus.EXCLUDED.value: self._("Excluded"),
                }
                smith_status_color = {
                    SmithStatus.INVITED.value: "orange",
                    SmithStatus.PENDING.value: "orange",
                    SmithStatus.SMITH.value: "green",
                    SmithStatus.EXCLUDED.value: "red",
                }
                smith_status_style = f"""
                                     color: white;
                                     border: 3px solid "{smith_status_color[self.smith.status.value]}";
                                     border-radius: 10%;
                                     background: "{smith_status_color[self.smith.status.value]}";
                                     """
                self.smithValuelabel.setText(
                    display_smith_status[self.smith.status.value]
                )
                self.smithValuelabel.setStyleSheet(smith_status_style)
                if self.smith.expire_on is not None:
                    expire_on_localized_datetime_string = self.locale().toString(
                        self.smith.expire_on,
                        QLocale.dateTimeFormat(self.locale(), QLocale.ShortFormat),
                    )
                    self.smithValuelabel.setToolTip(expire_on_localized_datetime_string)
                else:
                    self.smithValuelabel.setToolTip("")

                # smith tab
                if self.smith_widget is not None:
                    self.smith_widget.update_ui()
                else:
                    self.smith_widget = AccountSmithWidget(
                        self.application, self.account, self.smith, self
                    )
                    self.tabWidgetPage3.layout().addWidget(self.smith_widget)
                    self.smith_widget.setSizePolicy(
                        QSizePolicy.Expanding, QSizePolicy.Expanding
                    )
                    self.accountTabWidget.setTabVisible(self.SMITH_TAB_INDEX, True)
            else:
                self.smithValuelabel.setText(self._("No"))
                self.smithValuelabel.setStyleSheet("")

                # smith tab widget
                self.accountTabWidget.setTabVisible(self.SMITH_TAB_INDEX, False)
                if self.smith_widget is not None:
                    self.tabWidgetPage3.layout().removeWidget(self.smith_widget)
                    self.smith_widget.deleteLater()
                    self.smith_widget = None

            if self.account.name is None:
                self.accountNameValueLabel.setText("")
            else:
                self.accountNameValueLabel.setText(self.account.name)
        else:
            self.accountTabWidget.setTabVisible(self.IDENTITY_TAB_INDEX, False)
            self.identityStatusIconLabel.setPixmap(QPixmap(ICON_IDENTITY))
            self.identityNameValueLabel.setText("")
            self.identityValueLabel.setText(self._("No"))
            self.smithValuelabel.setText(self._("No"))
            self.identityValueLabel.setStyleSheet("")
            self.smithValuelabel.setStyleSheet("")
            if self.account.name is None:
                self.accountNameValueLabel.setText("")
            else:
                self.accountNameValueLabel.setText(self.account.name)

            # hide identity tab widget
            self.accountTabWidget.setTabVisible(self.IDENTITY_TAB_INDEX, False)
            if self.identity_widget is not None:
                self.tabWidgetPage2.layout().removeWidget(self.identity_widget)
                self.identity_widget.deleteLater()
                self.identity_widget = None

            # hide smith tab widget
            self.accountTabWidget.setTabVisible(self.SMITH_TAB_INDEX, False)
            if self.smith_widget is not None:
                self.tabWidgetPage3.layout().removeWidget(self.smith_widget)
                self.smith_widget.deleteLater()
                self.smith_widget = None

        if self.account.root is None and self.account.path is None:
            self.derivationValueLabel.setText(self._("Root"))
        else:
            root_account = self.application.accounts.get_by_address(self.account.root)
            if root_account is not None and root_account.name is not None:
                self.derivationValueLabel.setFont(QFont())
                self.derivationValueLabel.setText(root_account.name + self.account.path)
            else:
                self.derivationValueLabel.setFont(self.monospace_font)
                self.derivationValueLabel.setText(
                    DisplayAddress(self.account.root).shorten + self.account.path
                )

        # transfers widget update UI
        self.transfers_widget.update_ui()

        # technical committee tab
        if (
            self.application.technical_committee.get_member_by_address(
                self.account.address
            )
            is not None
        ):
            if self.technical_committee_widget is not None:
                self.technical_committee_widget.update_ui()
            else:
                self.technical_committee_widget = AccountTechnicalCommitteeWidget(
                    self.application, self.account, self
                )
                self.tabWidgetPage4.layout().addWidget(self.technical_committee_widget)
                self.technical_committee_widget.setSizePolicy(
                    QSizePolicy.Expanding, QSizePolicy.Expanding
                )
                self.accountTabWidget.setTabVisible(
                    self.TECHNICAL_COMMITTEE_TAB_INDEX, True
                )
        else:
            self.accountTabWidget.setTabVisible(
                self.TECHNICAL_COMMITTEE_TAB_INDEX, False
            )
            if self.technical_committee_widget is not None:
                self.tabWidgetPage4.layout().removeWidget(
                    self.technical_committee_widget
                )
                self.technical_committee_widget.deleteLater()
                self.technical_committee_widget = None

        # profile tab
        if self.profile is not None:
            if self.profile_widget is not None:
                self.profile_widget.update_ui()
            else:
                self.profile_widget = AccountProfileWidget(
                    self.application, self.account, self
                )
                self.tabWidgetPage5.layout().addWidget(self.profile_widget)
                self.profile_widget.setSizePolicy(
                    QSizePolicy.Expanding, QSizePolicy.Expanding
                )
                self.accountTabWidget.setTabVisible(self.PROFILE_TAB_INDEX, True)
        else:
            self.accountTabWidget.setTabVisible(self.PROFILE_TAB_INDEX, False)
            if self.profile_widget is not None:
                self.tabWidgetPage5.layout().removeWidget(self.profile_widget)
                self.profile_widget.deleteLater()
                self.profile_widget = None

        if not self.application.wallets.exists(self.account.address):
            self.transferFromButton.setEnabled(False)

        if not self.application.connections.node.is_connected():
            self.transferToButton.setEnabled(False)
            self.transferFromButton.setEnabled(False)

        # fix bug on tab titles not showing after setTabVisible()
        QTimer.singleShot(500, self._fix_tab_bar_geometry)

    def _fix_tab_bar_geometry(self):
        """
        Force Qt to calculate again the geometry of tab bar after setTabVisible()
        Fix bug when tab titles do not appear correctly
        """
        try:
            tab_bar = self.accountTabWidget.tabBar()
            if tab_bar:
                current_size = tab_bar.size()

                # Forcer un changement de taille
                tab_bar.resize(current_size.width() + 1, current_size.height())
                QApplication.processEvents()  # Important!
                tab_bar.resize(current_size.width(), current_size.height())
                QApplication.processEvents()
        except RuntimeError:
            # C++ object is destroy, can happen in tests
            pass

    def on_account_context_menu(self, position: QPoint):
        """
        When right button on account tab

        :return:
        """
        # display popup menu at click position
        menu = AccountPopupMenu(self.application, self.account, self)
        menu.exec_(self.mapToGlobal(position))

    def on_identities_updated_event_bus(self, identities: List[Identity]):
        """
        Triggered when identities are updated

        :param identities: List of Identity instance
        :return:
        """
        if self.account.address in [identity.address for identity in identities]:
            self._update_ui()

    def on_account_identities_updated_event_bus(self, accounts: List[Account]):
        """
        Triggered when account identities are updated (some identities are None, so we are using accounts!)

        :param accounts: List of Account instance
        :return:
        """
        if self.account.address in [account.address for account in accounts]:
            self._update_ui()

    def on_account_profiles_updated_event_bus(self, accounts: List[Account]):
        """
        Triggered when account profiles are updated (some profiles are None, so we are using accounts!)

        :param accounts: List of Account instance
        :return:
        """
        if self.account.address in [account.address for account in accounts]:
            self._update_ui()

    def on_account_balances_updated(self, accounts: List[Account]):
        """
        Triggered when an on_account_balances_updated event is dispatched

        :param accounts: List of Account instances
        :return:
        """
        if self.account.address in [account.address for account in accounts]:
            self._update_ui()

    def update_ui_per_account_on_event_bus(self, account: Account):
        """
        Triggered when an updated is needed per account

        :param account: Account instance
        :return:
        """
        if account.address == self.account.address:
            self._update_ui()

    def on_node_connection_changed_event_bus(self):
        """
        Triggered when the node connection has changed

        :return:
        """
        self._update_ui()

    def on_account_renamed_event_bus(self, _: Account):
        """
        Triggered when the account is renamed

        :param _: Account instance
        :return:
        """
        self._update_ui()

    def repository_data_updated_event_bus(self):
        """
        Triggered when the main window has updated repository data

        :return:
        """
        self._update_ui()

    def on_account_updated_event_bus(self, account: Account):
        """
        Triggered when the main window has updated repository data

        :param account: Account instance
        :return:
        """
        if account.address == self.account.address:
            self._update_ui()

    def on_currency_unit_changed_event_bus(self):
        """
        Triggered when user change money units

        :return:
        """
        self._update_ui()


if __name__ == "__main__":
    qapp = QApplication(sys.argv)

    application_ = Application(TEST_DATA_PATH)

    account_ = application_.accounts.create_new_root_account(
        mnemonic="bottom drive obey lake curtain smoke basket hold race lonely fit walk",
        language_code="en",
        crypto_type=AccountCryptoType.ED25519,
        name="Root ED",
        password="aaaaaa",
    )

    identity_ = application_.identities.create(1, 0, 0, account_.address)
    application_.identities.add(identity_)

    smith_ = application_.smiths.create(1)
    application_.smiths.add(smith_)

    application_.profiles.add(
        Profile(
            account_.address,
            OrderedDict(
                {
                    "hash": "B321DCE30F60A1A977C8D2FBA50B16C9CE1B42D59C4908709F49923628188D4C",
                    "signature": "m9YxEpz6yZVgaFe4s5Hmk7M5qHWUDOHqZIOycJfQAM5ZpyzVxCMygVlCq9sdomRtfE/k7ynLxDuz2/ScLyHxBA==",
                    "version": 2,
                    "title": "alice",
                    "issuer": "4XDM51AWupjKLWkVPxxJxrqN8DEhmPuY3S1FEepvHvxN",
                    "time": 1771626386,
                    "description": "auieauieauie !!!!!!auieuieauie",
                    "avatar": {},
                }
            ),
        )
    )

    main_window = QMainWindow()
    main_window.show()

    main_window.setCentralWidget(AccountWidget(application_, account_, main_window))

    sys.exit(qapp.exec_())
