import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtGui as qtg
from PyQt5 import QtCore as qtc
from PyQt5 import QtPrintSupport as qtps


class InvoiceForm(qtw.QWidget):

    submitted = qtc.pyqtSignal(dict)

    def __init__(self):
        super().__init__()
        self.setLayout(qtw.QFormLayout())
        self.inputs = dict()
        self.inputs['Customer Name'] = qtw.QLineEdit()
        self.inputs['Customer Address'] = qtw.QPlainTextEdit()
        self.inputs['Invoice Date'] = qtw.QDateEdit(
            date=qtc.QDate.currentDate(), calendarPopup=True)
        self.inputs['Days until Due'] = qtw.QSpinBox(
            minimum=0, maximum=60, value=30)
        for label, widget in self.inputs.items():
            self.layout().addRow(label, widget)

        self.line_items = qtw.QTableWidget(
            rowCount=10, columnCount=3)
        self.line_items.setHorizontalHeaderLabels(
            ['Job', 'Rate', 'Hours'])
        self.line_items.horizontalHeader().setSectionResizeMode(
            qtw.QHeaderView.Stretch)
        self.layout().addRow(self.line_items)
        for row in range(self.line_items.rowCount()):
            for col in range(self.line_items.columnCount()):
                if col > 0:
                    w = qtw.QSpinBox(minimum=0, maximum=300)
                    self.line_items.setCellWidget(row, col, w)
        submit = qtw.QPushButton('Create Invoice', clicked=self.on_submit)
        self.layout().addRow(submit)

        self.on_submit()

    def on_submit(self):
        data = {
            'c_name': self.inputs['Customer Name'].text(),
            'c_addr': self.inputs['Customer Address'].toPlainText(),
            'i_date': self.inputs['Invoice Date'].date().toString(),
            'i_due': self.inputs['Invoice Date'].date().addDays(
                self.inputs['Days until Due'].value()).toString(),
            'i_terms': '{} days'.format(self.inputs['Days until Due'].value())
        }
        data['line_items'] = list()
        for row in range(self.line_items.rowCount()):
            if not self.line_items.item(row, 0):
                continue
            job = self.line_items.item(row, 0).text()
            rate = self.line_items.cellWidget(row, 1).value()
            hours = self.line_items.cellWidget(row, 2).value()
            total = rate * hours
            row_data = [job, rate, hours, total]
            if any(row_data):
                data['line_items'].append(row_data)
        data['total_due'] = sum(x[3] for x in data['line_items'])
        self.submitted.emit(data)


class InvoiceView(qtw.QTextEdit):

    dpi = 72
    doc_width = 8.5 * dpi
    doc_height = 11 * dpi

    def __init__(self):
        super().__init__(readOnly=True)
        self.setFixedSize(qtc.QSize(self.doc_width, self.doc_height))


    def set_page_size(self, qrect):
        self.doc_width = qrect.width()
        self.doc_height = qrect.height()
        self.setFixedSize(qtc.QSize(self.doc_width, self.doc_height))
        self.document().setPageSize(
            qtc.QSizeF(self.doc_width, self.doc_height))

    def build_invoice(self, data):
        document = qtg.QTextDocument()
        self.setDocument(document)
        document.setPageSize(qtc.QSizeF(self.doc_width, self.doc_height))
        cursor = qtg.QTextCursor(document)
        root = document.rootFrame()
        cursor.setPosition(root.lastPosition())

        # Insert top-level frames
        logo_frame_fmt = qtg.QTextFrameFormat()
        logo_frame_fmt.setBorder(2)
        logo_frame_fmt.setPadding(10)
        logo_frame = cursor.insertFrame(logo_frame_fmt)

        cursor.setPosition(root.lastPosition())
        cust_addr_frame_fmt = qtg.QTextFrameFormat()
        cust_addr_frame_fmt.setWidth(self.doc_width * .3)
        cust_addr_frame_fmt.setPosition(qtg.QTextFrameFormat.FloatRight)
        cust_addr_frame = cursor.insertFrame(cust_addr_frame_fmt)

        cursor.setPosition(root.lastPosition())
        terms_frame_fmt = qtg.QTextFrameFormat()
        terms_frame_fmt.setWidth(self.doc_width * .5)
        terms_frame_fmt.setPosition(qtg.QTextFrameFormat.FloatLeft)
        terms_frame = cursor.insertFrame(terms_frame_fmt)

        cursor.setPosition(root.lastPosition())
        line_items_frame_fmt = qtg.QTextFrameFormat()
        line_items_frame_fmt.setMargin(25)
        line_items_frame = cursor.insertFrame(line_items_frame_fmt)

        # Create the heading
        # create a format for the characters
        std_format = qtg.QTextCharFormat()

        logo_format = qtg.QTextCharFormat()
        logo_format.setFont(
            qtg.QFont('Impact', 24, qtg.QFont.DemiBold))
        logo_format.setUnderlineStyle(
            qtg.QTextCharFormat.SingleUnderline)
        logo_format.setVerticalAlignment(
            qtg.QTextCharFormat.AlignMiddle)

        label_format = qtg.QTextCharFormat()
        label_format.setFont(qtg.QFont('Sans', 12, qtg.QFont.Bold))

        # create a format for the block
        cursor.setPosition(logo_frame.firstPosition())
        # The easy way:
        #cursor.insertImage('nc_logo.png')
        # The better way:
        logo_image_fmt = qtg.QTextImageFormat()
        logo_image_fmt.setName('nc_logo.png')
        logo_image_fmt.setHeight(48)
        cursor.insertImage(logo_image_fmt, qtg.QTextFrameFormat.FloatLeft)
        cursor.insertText('   ')
        cursor.insertText('Ninja Coders, LLC', logo_format)
        cursor.insertBlock()
        cursor.insertText('123 N Wizard St, Yonkers, NY 10701', std_format)

        ## Customer address
        cursor.setPosition(cust_addr_frame.lastPosition())

        address_format = qtg.QTextBlockFormat()
        address_format.setLineHeight(
            150, qtg.QTextBlockFormat.ProportionalHeight)
        address_format.setAlignment(qtc.Qt.AlignRight)
        address_format.setRightMargin(25)

        cursor.insertBlock(address_format)
        cursor.insertText('Customer:', label_format)
        cursor.insertBlock(address_format)
        cursor.insertText(data['c_name'], std_format)
        cursor.insertBlock(address_format)
        cursor.insertText(data['c_addr'])

        ## Terms
        cursor.setPosition(terms_frame.lastPosition())
        cursor.insertText('Terms:', label_format)
        cursor.insertList(qtg.QTextListFormat.ListDisc)
        # cursor is now in the first list item

        term_items = (
            f'<b>Invoice dated:</b> {data["i_date"]}',
            f'<b>Invoice terms:</b> {data["i_terms"]}',
            f'<b>Invoice due:</b> {data["i_due"]}',
        )

        for i, item in enumerate(term_items):
            if i > 0:
                cursor.insertBlock()
            # We can insert HTML too, but not with a textformat
            cursor.insertHtml(item)

        ## Line items
        table_format = qtg.QTextTableFormat()
        table_format.setHeaderRowCount(1)
        table_format.setWidth(
            qtg.QTextLength(qtg.QTextLength.PercentageLength, 100))

        headings = ('Job', 'Rate', 'Hours', 'Cost')
        num_rows = len(data['line_items']) + 1
        num_cols = len(headings)

        cursor.setPosition(line_items_frame.lastPosition())
        table = cursor.insertTable(num_rows, num_cols, table_format)

        # now we're in the first cell of the table
        # write headers
        for heading in headings:
            cursor.insertText(heading, label_format)
            cursor.movePosition(qtg.QTextCursor.NextCell)

        # write data
        for row in data['line_items']:
            for col, value in enumerate(row):
                text = f'${value}' if col in (1, 3) else f'{value}'
                cursor.insertText(text, std_format)
                cursor.movePosition(qtg.QTextCursor.NextCell)

        # Append a row
        table.appendRows(1)
        cursor = table.cellAt(num_rows, 0).lastCursorPosition()
        cursor.insertText('Total', label_format)
        cursor = table.cellAt(num_rows, 3).lastCursorPosition()
        cursor.insertText(f"${data['total_due']}", label_format)


class MainWindow(qtw.QMainWindow):

    def __init__(self):
        """MainWindow constructor."""
        super().__init__()
        # Main UI code goes here
        main = qtw.QWidget()
        main.setLayout(qtw.QHBoxLayout())
        self.setCentralWidget(main)

        form = InvoiceForm()
        main.layout().addWidget(form)

        self.preview = InvoiceView()
        main.layout().addWidget(self.preview)

        form.submitted.connect(self.preview.build_invoice)

        # Printing
        print_tb = self.addToolBar('Printing')
        print_tb.addAction('Configure Printer', self.printer_config)
        print_tb.addAction('Print Preview', self.print_preview)
        print_tb.addAction('Print dialog', self.print_dialog)
        print_tb.addAction('Export PDF', self.export_pdf)

        self.printer = qtps.QPrinter()
        # Configure defaults:
        self.printer.setOrientation(qtps.QPrinter.Portrait)
        self.printer.setPageSize(qtg.QPageSize(qtg.QPageSize.Letter))
        self._update_preview_size()


        # End main UI code
        self.show()

    def _update_preview_size(self):
        self.preview.set_page_size(
            self.printer.pageRect(qtps.QPrinter.Point))

    def printer_config(self):
        dialog = qtps.QPageSetupDialog(self.printer, self)
        dialog.exec()
        self._update_preview_size()

    def _print_document(self):
        # doesn't actually kick off printer,
        # just paints document to the printer object
        self.preview.document().print(self.printer)

    def print_dialog(self):
        # Errata:  the book contained this line:
        #self._print_document()
        # As noted by DevinLand in issue #8, this can cause the document to start printing.
        dialog = qtps.QPrintDialog(self.printer, self)

        # Instead we'll add this line, so _print_document is triggered when the dialog is
        # accepted:
        dialog.accepted.connect(self._print_document)
        dialog.exec()
        self._update_preview_size()

    def print_preview(self):
        dialog = qtps.QPrintPreviewDialog(self.printer, self)
        dialog.paintRequested.connect(self._print_document)
        dialog.exec()
        self._update_preview_size()

    def export_pdf(self):
        filename, _ = qtw.QFileDialog.getSaveFileName(
            self, "Save to PDF", qtc.QDir.homePath(), "PDF Files (*.pdf)")
        if filename:
            self.printer.setOutputFileName(filename)
            self.printer.setOutputFormat(qtps.QPrinter.PdfFormat)
            self._print_document()


if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    mw = MainWindow()
    sys.exit(app.exec())