# Copyright (C) 2013 Riverbank Computing Limited.
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import pickle
import sys

from PySide6.QtCore import QFile, QIODevice, QTextStream, Qt, Slot
from PySide6.QtWidgets import (QApplication, QDialog, QFileDialog,
                               QGridLayout, QHBoxLayout, QLabel, QLineEdit,
                               QMessageBox, QPushButton, QTextEdit,
                               QVBoxLayout, QWidget)


class SortedDict(dict):
    class Iterator(object):
        def __init__(self, sorted_dict):
            self._dict = sorted_dict
            self._keys = sorted(self._dict.keys())
            self._nr_items = len(self._keys)
            self._idx = 0

        def __iter__(self):
            return self

        def next(self):
            if self._idx >= self._nr_items:
                raise StopIteration

            key = self._keys[self._idx]
            value = self._dict[key]
            self._idx += 1

            return key, value

        __next__ = next

    def __iter__(self):
        return SortedDict.Iterator(self)

    iterkeys = __iter__


class AddressBook(QWidget):
    NavigationMode, AddingMode, EditingMode = range(3)

    def __init__(self, parent=None):
        super().__init__(parent)

        self.contacts = SortedDict()
        self._old_name = ''
        self._old_address = ''
        self._current_mode = self.NavigationMode

        name_label = QLabel("Name:")
        self._name_line = QLineEdit()
        self._name_line.setReadOnly(True)

        address_label = QLabel("Address:")
        self._address_text = QTextEdit()
        self._address_text.setReadOnly(True)

        self._add_button = QPushButton("&Add")
        self._edit_button = QPushButton("&Edit")
        self._edit_button.setEnabled(False)
        self._remove_button = QPushButton("&Remove")
        self._remove_button.setEnabled(False)
        self._find_button = QPushButton("&Find")
        self._find_button.setEnabled(False)
        self._submit_button = QPushButton("&Submit")
        self._submit_button.hide()
        self._cancel_button = QPushButton("&Cancel")
        self._cancel_button.hide()

        self._next_button = QPushButton("&Next")
        self._next_button.setEnabled(False)
        self._previous_button = QPushButton("&Previous")
        self._previous_button.setEnabled(False)

        self._load_button = QPushButton("&Load...")
        self._load_button.setToolTip("Load contacts from a file")
        self._save_button = QPushButton("Sa&ve...")
        self._save_button.setToolTip("Save contacts to a file")
        self._save_button.setEnabled(False)

        self._export_button = QPushButton("Ex&port")
        self._export_button.setToolTip("Export as vCard")
        self._export_button.setEnabled(False)

        self.dialog = FindDialog()

        self._add_button.clicked.connect(self.add_contact)
        self._submit_button.clicked.connect(self.submit_contact)
        self._edit_button.clicked.connect(self.edit_contact)
        self._remove_button.clicked.connect(self.remove_contact)
        self._find_button.clicked.connect(self.find_contact)
        self._cancel_button.clicked.connect(self.cancel)
        self._next_button.clicked.connect(self.next)
        self._previous_button.clicked.connect(self.previous)
        self._load_button.clicked.connect(self.load_from_file)
        self._save_button.clicked.connect(self.save_to_file)
        self._export_button.clicked.connect(self.export_as_vcard)

        button_layout_1 = QVBoxLayout()
        button_layout_1.addWidget(self._add_button)
        button_layout_1.addWidget(self._edit_button)
        button_layout_1.addWidget(self._remove_button)
        button_layout_1.addWidget(self._find_button)
        button_layout_1.addWidget(self._submit_button)
        button_layout_1.addWidget(self._cancel_button)
        button_layout_1.addWidget(self._load_button)
        button_layout_1.addWidget(self._save_button)
        button_layout_1.addWidget(self._export_button)
        button_layout_1.addStretch()

        button_layout_2 = QHBoxLayout()
        button_layout_2.addWidget(self._previous_button)
        button_layout_2.addWidget(self._next_button)

        main_layout = QGridLayout()
        main_layout.addWidget(name_label, 0, 0)
        main_layout.addWidget(self._name_line, 0, 1)
        main_layout.addWidget(address_label, 1, 0, Qt.AlignTop)
        main_layout.addWidget(self._address_text, 1, 1)
        main_layout.addLayout(button_layout_1, 1, 2)
        main_layout.addLayout(button_layout_2, 2, 1)

        self.setLayout(main_layout)
        self.setWindowTitle("Simple Address Book")

    @Slot()
    def add_contact(self):
        self._old_name = self._name_line.text()
        self._old_address = self._address_text.toPlainText()

        self._name_line.clear()
        self._address_text.clear()

        self.update_interface(self.AddingMode)

    @Slot()
    def edit_contact(self):
        self._old_name = self._name_line.text()
        self._old_address = self._address_text.toPlainText()

        self.update_interface(self.EditingMode)

    @Slot()
    def submit_contact(self):
        name = self._name_line.text()
        address = self._address_text.toPlainText()

        if name == "" or address == "":
            QMessageBox.information(self, "Empty Field", "Please enter a name and address.")
            return

        if self._current_mode == self.AddingMode:
            if name not in self.contacts:
                self.contacts[name] = address
                QMessageBox.information(self, "Add Successful",
                                        f'"{name}" has been added to your address book.')
            else:
                QMessageBox.information(self, "Add Unsuccessful",
                                        f'Sorry, "{name}" is already in your address book.')
                return

        elif self._current_mode == self.EditingMode:
            if self._old_name != name:
                if name not in self.contacts:
                    QMessageBox.information(self, "Edit Successful",
                                            f'"{self.oldName}" has been edited in your '
                                            'address book.')
                    del self.contacts[self._old_name]
                    self.contacts[name] = address
                else:
                    QMessageBox.information(self, "Edit Unsuccessful",
                                            f'Sorry, "{name}" is already in your address book.')
                    return
            elif self._old_address != address:
                QMessageBox.information(self, "Edit Successful",
                                        f'"{name}" has been edited in your address book.')
                self.contacts[name] = address

        self.update_interface(self.NavigationMode)

    @Slot()
    def cancel(self):
        self._name_line.setText(self._old_name)
        self._address_text.setText(self._old_address)
        self.update_interface(self.NavigationMode)

    @Slot()
    def remove_contact(self):
        name = self._name_line.text()

        if name in self.contacts:
            button = QMessageBox.question(self, "Confirm Remove",
                                          f'Are you sure you want to remove "{name}"?',
                                          QMessageBox.Yes | QMessageBox.No)

            if button == QMessageBox.Yes:
                self.previous()
                del self.contacts[name]

                QMessageBox.information(self, "Remove Successful",
                                        f'"{name}" has been removed from your address book.')

        self.update_interface(self.NavigationMode)

    @Slot()
    def next(self):
        name = self._name_line.text()
        it = iter(self.contacts)

        try:
            while True:
                this_name, _ = it.next()

                if this_name == name:
                    next_name, next_address = it.next()
                    break
        except StopIteration:
            next_name, next_address = iter(self.contacts).next()

        self._name_line.setText(next_name)
        self._address_text.setText(next_address)

    @Slot()
    def previous(self):
        name = self._name_line.text()

        prev_name = prev_address = None
        for this_name, this_address in self.contacts:
            if this_name == name:
                break

            prev_name = this_name
            prev_address = this_address
        else:
            self._name_line.clear()
            self._address_text.clear()
            return

        if prev_name is None:
            for prev_name, prev_address in self.contacts:
                pass

        self._name_line.setText(prev_name)
        self._address_text.setText(prev_address)

    def find_contact(self):
        self.dialog.show()

        if self.dialog.exec() == QDialog.Accepted:
            contact_name = self.dialog.get_find_text()

            if contact_name in self.contacts:
                self._name_line.setText(contact_name)
                self._address_text.setText(self.contacts[contact_name])
            else:
                QMessageBox.information(self, "Contact Not Found",
                                        f'Sorry, "{contact_name}" is not in your address book.')
                return

        self.update_interface(self.NavigationMode)

    def update_interface(self, mode):
        self._current_mode = mode

        if self._current_mode in (self.AddingMode, self.EditingMode):
            self._name_line.setReadOnly(False)
            self._name_line.setFocus(Qt.OtherFocusReason)
            self._address_text.setReadOnly(False)

            self._add_button.setEnabled(False)
            self._edit_button.setEnabled(False)
            self._remove_button.setEnabled(False)

            self._next_button.setEnabled(False)
            self._previous_button.setEnabled(False)

            self._submit_button.show()
            self._cancel_button.show()

            self._load_button.setEnabled(False)
            self._save_button.setEnabled(False)
            self._export_button.setEnabled(False)

        elif self._current_mode == self.NavigationMode:
            if not self.contacts:
                self._name_line.clear()
                self._address_text.clear()

            self._name_line.setReadOnly(True)
            self._address_text.setReadOnly(True)
            self._add_button.setEnabled(True)

            number = len(self.contacts)
            self._edit_button.setEnabled(number >= 1)
            self._remove_button.setEnabled(number >= 1)
            self._find_button.setEnabled(number > 2)
            self._next_button.setEnabled(number > 1)
            self._previous_button.setEnabled(number > 1)

            self._submit_button.hide()
            self._cancel_button.hide()

            self._export_button.setEnabled(number >= 1)

            self._load_button.setEnabled(True)
            self._save_button.setEnabled(number >= 1)

    def save_to_file(self):
        fileName, _ = QFileDialog.getSaveFileName(self,
                                                  "Save Address Book", '',
                                                  "Address Book (*.abk);;All Files (*)")

        if not fileName:
            return

        try:
            out_file = open(str(fileName), 'wb')
        except IOError:
            QMessageBox.information(self, "Unable to open file",
                                    f'There was an error opening "{fileName}"')
            return

        pickle.dump(self.contacts, out_file)
        out_file.close()

    def load_from_file(self):
        fileName, _ = QFileDialog.getOpenFileName(self,
                                                  "Open Address Book", '',
                                                  "Address Book (*.abk);;All Files (*)")

        if not fileName:
            return

        try:
            in_file = open(str(fileName), 'rb')
        except IOError:
            QMessageBox.information(self, "Unable to open file",
                                    f'There was an error opening "{fileName}"')
            return

        self.contacts = pickle.load(in_file)
        in_file.close()

        if len(self.contacts) == 0:
            QMessageBox.information(self, "No contacts in file",
                                    "The file you are attempting to open contains no contacts.")
        else:
            for name, address in self.contacts:
                self._name_line.setText(name)
                self._address_text.setText(address)

        self.update_interface(self.NavigationMode)

    def export_as_vcard(self):
        name = str(self._name_line.text())
        address = self._address_text.toPlainText()

        name_list = name.split()

        if len(name_list) > 1:
            first_name = name_list[0]
            last_name = name_list[-1]
        else:
            first_name = name
            last_name = ''

        file_name = QFileDialog.getSaveFileName(self, "Export Contact",
                                                '', "vCard Files (*.vcf);;All Files (*)")[0]

        if not file_name:
            return

        out_file = QFile(file_name)

        if not out_file.open(QIODevice.WriteOnly):
            QMessageBox.information(self, "Unable to open file", out_file.errorString())
            return

        out_s = QTextStream(out_file)

        out_s << 'BEGIN:VCARD' << '\n'
        out_s << 'VERSION:2.1' << '\n'
        out_s << 'N:' << last_name << ';' << first_name << '\n'
        out_s << 'FN:' << ' '.join(name_list) << '\n'

        address.replace(';', '\\;')
        address.replace('\n', ';')
        address.replace(',', ' ')

        out_s << 'ADR;HOME:;' << address << '\n'
        out_s << 'END:VCARD' << '\n'

        QMessageBox.information(self, "Export Successful",
                                f'"{name}" has been exported as a vCard.')


class FindDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)

        find_label = QLabel("Enter the name of a contact:")
        self._line_edit = QLineEdit()

        self._find_button = QPushButton("&Find")
        self._find_text = ''

        layout = QHBoxLayout()
        layout.addWidget(find_label)
        layout.addWidget(self._line_edit)
        layout.addWidget(self._find_button)

        self.setLayout(layout)
        self.setWindowTitle("Find a Contact")

        self._find_button.clicked.connect(self.find_clicked)
        self._find_button.clicked.connect(self.accept)

    def find_clicked(self):
        text = self._line_edit.text()

        if not text:
            QMessageBox.information(self, "Empty Field", "Please enter a name.")
            return

        self._find_text = text
        self._line_edit.clear()
        self.hide()

    def get_find_text(self):
        return self._find_text


if __name__ == '__main__':
    app = QApplication(sys.argv)

    address_book = AddressBook()
    address_book.show()

    sys.exit(app.exec())
