# Copyright (C) 2020 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php

import time
from datetime import datetime
from typing import Optional, Any

from PyQt6.QtCore import Qt, QDateTime, pyqtSignal
from PyQt6.QtGui import QPalette, QPainter
from PyQt6.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox,
                             QHBoxLayout, QDateTimeEdit)

from electrum.i18n import _
from electrum.bitcoin import NLOCKTIME_MIN, NLOCKTIME_MAX, NLOCKTIME_BLOCKHEIGHT_MAX

from .util import char_width_in_lineedit, ColorScheme


class LockTimeEdit(QWidget):

    valueEdited = pyqtSignal()

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

        hbox = QHBoxLayout()
        self.setLayout(hbox)
        hbox.setContentsMargins(0, 0, 0, 0)
        hbox.setSpacing(0)

        self.locktime_raw_e = LockTimeRawEdit(self)
        self.locktime_height_e = LockTimeHeightEdit(self)
        self.locktime_date_e = LockTimeDateEdit(self)
        self.editors = [self.locktime_raw_e, self.locktime_height_e, self.locktime_date_e]

        self.combo = QComboBox()
        options = [_("Raw"), _("Block height"), _("Date")]
        option_index_to_editor_map = {
            0: self.locktime_raw_e,
            1: self.locktime_height_e,
            2: self.locktime_date_e,
        }
        default_index = 1
        self.combo.addItems(options)

        def on_current_index_changed(i):
            for w in self.editors:
                w.setVisible(False)
                w.setEnabled(False)
            prev_locktime = self.editor.get_locktime()
            self.editor = option_index_to_editor_map[i]
            if self.editor.is_acceptable_locktime(prev_locktime):
                self.editor.set_locktime(prev_locktime)
            self.editor.setVisible(True)
            self.editor.setEnabled(True)

        self.editor = option_index_to_editor_map[default_index]
        self.combo.currentIndexChanged.connect(on_current_index_changed)
        self.combo.setCurrentIndex(default_index)
        on_current_index_changed(default_index)

        hbox.addWidget(self.combo)
        for w in self.editors:
            hbox.addWidget(w)
        hbox.addStretch(1)

        self.locktime_height_e.textEdited.connect(self.valueEdited.emit)
        self.locktime_raw_e.textEdited.connect(self.valueEdited.emit)
        self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit)
        self.combo.currentIndexChanged.connect(self.valueEdited.emit)

    def get_locktime(self) -> Optional[int]:
        return self.editor.get_locktime()

    def set_locktime(self, x: Any) -> None:
        self.editor.set_locktime(x)


class _LockTimeEditor:
    min_allowed_value = NLOCKTIME_MIN
    max_allowed_value = NLOCKTIME_MAX

    def get_locktime(self) -> Optional[int]:
        raise NotImplementedError()

    def set_locktime(self, x: Any) -> None:
        raise NotImplementedError()

    @classmethod
    def is_acceptable_locktime(cls, x: Any) -> bool:
        if not x:  # e.g. empty string
            return True
        try:
            x = int(x)
        except Exception:
            return False
        return cls.min_allowed_value <= x <= cls.max_allowed_value


class LockTimeRawEdit(QLineEdit, _LockTimeEditor):

    def __init__(self, parent=None):
        QLineEdit.__init__(self, parent)
        self.setFixedWidth(14 * char_width_in_lineedit())
        self.textChanged.connect(self.numbify)

    def numbify(self):
        text = self.text().strip()
        chars = '0123456789'
        pos = self.cursorPosition()
        pos = len(''.join([i for i in text[:pos] if i in chars]))
        s = ''.join([i for i in text if i in chars])
        self.set_locktime(s)
        # setText sets Modified to False.  Instead we want to remember
        # if updates were because of user modification.
        self.setModified(self.hasFocus())
        self.setCursorPosition(pos)

    def get_locktime(self) -> Optional[int]:
        try:
            return int(str(self.text()))
        except Exception:
            return None

    def set_locktime(self, x: Any) -> None:
        try:
            x = int(x)
        except Exception:
            self.setText('')
            return
        x = max(x, self.min_allowed_value)
        x = min(x, self.max_allowed_value)
        self.setText(str(x))


class LockTimeHeightEdit(LockTimeRawEdit):
    max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX

    def __init__(self, parent=None):
        LockTimeRawEdit.__init__(self, parent)
        self.setFixedWidth(20 * char_width_in_lineedit())

    def paintEvent(self, event):
        super().paintEvent(event)
        panel = QStyleOptionFrame()
        self.initStyleOption(panel)
        textRect = self.style().subElementRect(QStyle.SubElement.SE_LineEditContents, panel, self)
        textRect.adjust(2, 0, -10, 0)
        painter = QPainter(self)
        painter.setPen(ColorScheme.GRAY.as_color())
        painter.drawText(textRect, int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), "height")


def get_max_allowed_timestamp() -> int:
    ts = NLOCKTIME_MAX
    # Test if this value is within the valid timestamp limits (which is platform-dependent).
    # see #6170
    try:
        datetime.fromtimestamp(ts)
    except (OSError, OverflowError):
        ts = 2 ** 31 - 1  # INT32_MAX
        datetime.fromtimestamp(ts)  # test if raises
    return ts


class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
    min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1
    max_allowed_value = get_max_allowed_timestamp()

    def __init__(self, parent=None):
        QDateTimeEdit.__init__(self, parent)
        self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value))
        self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value))
        self.setDateTime(QDateTime.currentDateTime())

    def get_locktime(self) -> Optional[int]:
        dt = self.dateTime().toPyDateTime()
        locktime = int(time.mktime(dt.timetuple()))
        return locktime

    def set_locktime(self, x: Any) -> None:
        if not self.is_acceptable_locktime(x):
            self.setDateTime(QDateTime.currentDateTime())
            return
        try:
            x = int(x)
        except Exception:
            self.setDateTime(QDateTime.currentDateTime())
            return
        dt = datetime.fromtimestamp(x)
        self.setDateTime(dt)
