import time
from typing import List, Optional
from iowp.special import SpecialModeFunction
from iowp.devices import IowDevice

class HD44780(SpecialModeFunction):
    """
    Implementation of the LCD special mode function for HD44780 controllers.
    Matches de.wagner_ibw.iow.lcd.HD44780.
    """
    FIRST_CTRL = 0
    SECOND_CTRL = 0x40
    NO_CTRL = 0

    def __init__(self):
        super().__init__()
        self.rows = 0
        self.cols = 0
        self.physical_rows = 0
        self.line_start_adr = []
        self.disp_on = True
        self.cursor_on = False
        self.char_blinking = False

    def get_name(self) -> str:
        return "LCD"

    def get_special_mode_function_id(self) -> int:
        return self.SMF_LCD_ID

    def get_report_ids(self) -> List[int]:
        return [0x06] # Note: Java code says 0x06, but matchReportId returns false. 
                      # This ID is for read reports which are handled by the device.

    def report_received(self, read_buffer: List[int]):
        pass # Not interested in reports

    def get_enable_report(self) -> List[int]:
        return [0x04, 0x01]

    def get_disable_report(self) -> List[int]:
        return [0x04, 0x00]

    def get_iow_special_bits(self, product_id: int) -> List[int]:
        if product_id == IowDevice.IOW24ID:
            return [0xf0, 0xff]
        elif product_id == IowDevice.IOW40ID:
            return [0x3c, 0xff]
        elif product_id == IowDevice.IOW56ID:
            return [0x00, 0x00, 0x00, 0xff, 0x1f]   #p3=data, p4=ctrl 
        elif product_id == IowDevice.IOW28ID or product_id == IowDevice.IOW28LID:
            return [0xf0, 0xff, 0x00, 0x00]
        elif product_id == IowDevice.IOW100ID:
            return [0x00, 0x00, 0x00, 0x00, 0x0f, 0xff] #p4=ctrl, p5=data
        return []


    def set_iow_device(self, iow):
        super().set_iow_device(iow)
        if iow:
            self.init_lcd(self.FIRST_CTRL)

    def write_cmd(self, cmd: int, ctrl: int = FIRST_CTRL) -> int:
        wbuf = [0x05, 0x01 | ctrl, cmd]
        return self.iow.write_report(1, wbuf)

    def write_data(self, data: int, ctrl: int = FIRST_CTRL) -> int:
        wbuf = [0x05, 0x81 | ctrl, data]
        return self.iow.write_report(1, wbuf)

    def init_lcd(self, ctrl: int = FIRST_CTRL) -> int:
        wbuf = [0x05, 0x03 | ctrl, 0x34, 0x01, 0x0F]
        if self.physical_rows > 1:
            wbuf[2] |= 0x08
        return self.iow.write_report(1, wbuf)

    def clear_lcd(self, ctrl: int = FIRST_CTRL):
        self.write_cmd(0x01, ctrl)
        time.sleep(0.002)

    def set_cursor_home(self, ctrl: int = FIRST_CTRL):
        self.write_cmd(0x02, ctrl)
        time.sleep(0.002)

    def set_entry_mode(self, move_forward: bool, shift_disp: bool, ctrl: int = FIRST_CTRL):
        cmd = 0x04
        if move_forward: cmd |= 0x02
        if shift_disp:   cmd |= 0x01
        self.write_cmd(cmd, ctrl)

    def set_display_control(self, disp_on: bool, cursor_on: bool, char_blinking: bool, ctrl: int = FIRST_CTRL):
        self.disp_on = disp_on
        self.cursor_on = cursor_on
        self.char_blinking = char_blinking
        cmd = 0x08
        if char_blinking: cmd |= 0x01
        if cursor_on:     cmd |= 0x02
        if disp_on:       cmd |= 0x04
        self.write_cmd(cmd, ctrl)

    def set_shift_control(self, shift_disp: bool, shift_dir: bool, ctrl: int = FIRST_CTRL):
        cmd = 0x10
        if shift_dir:  cmd |= 0x04
        if shift_disp: cmd |= 0x08
        self.write_cmd(cmd, ctrl)

    def write_line(self, row: int, col_or_clear, clear_or_str=None, str_val=None):
        """
        Implementation of the overloaded writeLine methods.
        Matches:
        public void writeLine(int row, boolean clear, String str)
        public void writeLine(int row, int col, boolean clear, String str)
        """
        if str_val is not None:
            # writeLine(int row, int col, boolean clear, String str)
            col = col_or_clear
            clear = clear_or_str
            str_data = str_val
        elif isinstance(clear_or_str, str):
            # writeLine(int row, boolean clear, String str)
            col = 1
            clear = col_or_clear
            str_data = clear_or_str
        else:
            raise ValueError("Invalid arguments for write_line")

        if row > self.rows:
            raise ValueError(f"Only row 1...{self.rows} allowed!")
        if col > self.cols:
            raise ValueError(f"Only column 1...{self.cols} allowed!")

        if clear:
            self.set_ddram_addr(self.line_start_adr[row - 1] + col - 1, self.FIRST_CTRL)
            padding = " " * (self.cols - (col - 1))
            self.write_string(padding)

        available = int(self.cols - (col - 1))
        trimmed_str = str_data[0:available]
        self.set_ddram_addr(self.line_start_adr[row - 1] + col - 1, self.FIRST_CTRL)
        self.write_string(trimmed_str)

    def write_string(self, str_data: str, ctrl: int = FIRST_CTRL):
        if not str_data:
            return
            
        payload_size = self.iow._smf_report_length - 2
        data_bytes = [ord(c) for c in str_data]
        pointer = 0
        total = len(data_bytes)

        while pointer < total:
            chunk_len = min(total - pointer, payload_size)
            wbuf = [0] * self.iow._smf_report_length
            wbuf[0] = 0x05
            wbuf[1] = 0x80 | ctrl | chunk_len
            
            for i in range(chunk_len):
                wbuf[i + 2] = data_bytes[pointer + i]
            
            self.iow.write_report(1, wbuf)
            pointer += chunk_len

    def set_ddram_addr(self, address: int, ctrl: int = FIRST_CTRL):
        cmd = 0x80 | address
        return self.write_cmd(cmd, ctrl)

    def set_cgram_addr(self, address: int, ctrl: int = FIRST_CTRL):
        cmd = 0x40 | address
        return self.write_cmd(cmd, ctrl)

    def set_cursor(self, row: int, col: int):
        if row > self.rows:
            raise ValueError(f"Only row 1...{self.rows} allowed!")
        address = self.line_start_adr[row - 1] + col - 1
        self.set_ddram_addr(address, self.FIRST_CTRL)

    def set_cursor_disp_on(self):
        self.set_display_control(True, self.cursor_on, self.char_blinking)

    def set_disp_off(self):
        self.set_display_control(False, self.cursor_on, self.char_blinking)

    def set_cursor_on(self):
        self.set_display_control(self.disp_on, True, self.char_blinking)

    def set_cursor_off(self):
        self.set_display_control(self.disp_on, False, self.char_blinking)

    def set_cursor_left(self):
        self.write_cmd(0x10, self.FIRST_CTRL)

    def set_cursor_right(self):
        self.write_cmd(0x14, self.FIRST_CTRL)

    def move_sprite(self, row: int, sprites: List[str], wait_ms: int):
        if row > self.rows:
            raise ValueError(f"Only row 1...{self.rows} allowed!")
            
        count_steps = len(sprites)
        self.write_line(row, True, "")
        
        step = 0
        space = ""
        for i in range(int(self.cols) + 1):
            if i != 0:
                space = f" {space}"
            
            self.write_line(row, False, f"{space}{sprites[step]}")
            step += 1
            if step == count_steps:
                step = 0
            
            time.sleep(wait_ms / 1000.0)

    def set_special_char(self, code: int, pattern: List[int], ctrl: int = FIRST_CTRL):
        if not (0 <= code <= 7):
            raise ValueError("Only code 0...7 allowed!")
        if len(pattern) != 8:
            raise ValueError("Pattern must be 8 lines!")

        self.set_cgram_addr(code * 8, ctrl)
        
        # Write first 6 bytes
        wbuf1 = [0x05, 0x86 | ctrl] + list(pattern[0:6])
        self.iow.write_report(1, wbuf1)
        
        # Write remaining 2 bytes
        wbuf2 = [0x05, 0x82 | ctrl] + list(pattern[6:8]) + [0, 0, 0, 0]
        self.iow.write_report(1, wbuf2)

class DoubleHD44780Iow56(HD44780):
    """
    LCD support for double controller modules using IOW56 Dual44780 mode.
    """
    def __init__(self):
        super().__init__()
        self.current_ctrl = self.FIRST_CTRL

    def clear_lcd(self, ctrl: int = HD44780.FIRST_CTRL):
        self.write_double_cmd(0x01)
        time.sleep(0.002)

    def set_cursor_home(self, ctrl: int = HD44780.FIRST_CTRL):
        self.write_cmd(0x02, self.FIRST_CTRL)
        time.sleep(0.002)

    def set_entry_mode(self, move_forward: bool, shift_disp: bool, ctrl: int = HD44780.FIRST_CTRL):
        cmd = 0x04
        if move_forward: cmd |= 0x02
        if shift_disp:   cmd |= 0x01
        self.write_double_cmd(cmd)

    def set_display_control(self, disp_on: bool, cursor_on: bool, char_blinking: bool, ctrl: int = HD44780.FIRST_CTRL):
        self.disp_on = disp_on
        self.cursor_on = cursor_on
        self.char_blinking = char_blinking
        cmd = 0x08
        if char_blinking: cmd |= 0x01
        if cursor_on:     cmd |= 0x02
        if disp_on:       cmd |= 0x04
        self.write_double_cmd(cmd)

    def set_shift_control(self, shift_disp: bool, shift_dir: bool, ctrl: int = HD44780.FIRST_CTRL):
        cmd = 0x10
        if shift_dir:  cmd |= 0x04
        if shift_disp: cmd |= 0x08
        self.write_double_cmd(cmd)

    def write_line(self, row: int, col_or_clear, clear_or_str=None, str_val=None):
        if str_val is not None:
            col, clear, str_data = col_or_clear, clear_or_str, str_val
        elif isinstance(clear_or_str, str):
            col, clear, str_data = 1, col_or_clear, clear_or_str
        else:
            raise ValueError("Invalid arguments")

        if row > self.rows: raise ValueError(f"Max row {self.rows}")
        
        ctrl = self.FIRST_CTRL if row < 3 else self.SECOND_CTRL
        
        if clear:
            self.set_ddram_addr(self.line_start_adr[row-1] + col - 1, ctrl)
            self.write_string(" " * (self.cols - (col - 1)), ctrl)
            
        available = int(self.cols - (col - 1))
        self.set_ddram_addr(self.line_start_adr[row-1] + col - 1, ctrl)
        text_to_write = str(str_data)
        self.write_string(text_to_write[0:available], ctrl)

    def set_cursor(self, row: int, col: int):
        if row > self.rows: raise ValueError(f"Max row {self.rows}")
        ctrl = self.FIRST_CTRL if row < 3 else self.SECOND_CTRL
        self.set_ddram_addr(self.line_start_adr[row-1] + col - 1, ctrl)

    def set_special_char(self, code: int, pattern: List[int], ctrl: int = HD44780.FIRST_CTRL):
        super().set_special_char(code, pattern, self.FIRST_CTRL)
        super().set_special_char(code, pattern, self.SECOND_CTRL)

    def get_enable_report(self) -> List[int]:
        return [0x04, 0x01, 0x01]

    def set_iow_device(self, iow):
        super(HD44780, self).set_iow_device(iow)
        if iow:
            self.init_lcd(self.FIRST_CTRL)
            self.init_lcd(self.SECOND_CTRL)

    def write_double_cmd(self, cmd: int):
        self.write_cmd(cmd, self.FIRST_CTRL)
        self.write_cmd(cmd, self.SECOND_CTRL)

class DoubleHD44780Ahw(HD44780):
    """
    LCD support for double controller modules using Additional Hardware (AHW) toggle pin.
    """
    def __init__(self, enable_port: int, enable_bit: int):
        super().__init__()
        self.enable_port = enable_port
        self.enable_bit = enable_bit
        self.current_enable = 0

    def clear_lcd(self, ctrl: int = HD44780.FIRST_CTRL):
        self.write_double_cmd(0x01)
        time.sleep(0.002)

    def set_cursor_home(self, ctrl: int = HD44780.FIRST_CTRL):
        self.write_cmd(0x02, self.NO_CTRL)
        time.sleep(0.002)

    def set_entry_mode(self, move_forward: bool, shift_disp: bool, ctrl: int = HD44780.FIRST_CTRL):
        cmd = 0x04
        if move_forward: cmd |= 0x02
        if shift_disp:   cmd |= 0x01
        self.write_double_cmd(cmd)

    def set_display_control(self, disp_on: bool, cursor_on: bool, char_blinking: bool, ctrl: int = HD44780.FIRST_CTRL):
        self.disp_on = disp_on
        self.cursor_on = cursor_on
        self.char_blinking = char_blinking
        cmd = 0x08
        if char_blinking: cmd |= 0x01
        if cursor_on:     cmd |= 0x02
        if disp_on:       cmd |= 0x04
        self.write_double_cmd(cmd)

    def set_shift_control(self, shift_disp: bool, shift_dir: bool, ctrl: int = HD44780.FIRST_CTRL):
        cmd = 0x10
        if shift_dir:  cmd |= 0x04
        if shift_disp: cmd |= 0x08
        self.write_double_cmd(cmd)

    def write_line(self, row: int, col_or_clear, clear_or_str=None, str_val=None):
        if str_val is not None:
            col, clear, str_data = col_or_clear, clear_or_str, str_val
        elif isinstance(clear_or_str, str):
            col, clear, str_data = 1, col_or_clear, clear_or_str
        else:
            raise ValueError("Invalid arguments")

        if row > self.rows: raise ValueError(f"Max row {self.rows}")
        
        if row < 3: self.enable_e1()
        else: self.enable_e2()
        
        if clear:
            self.set_ddram_addr(self.line_start_adr[row-1] + col - 1, self.NO_CTRL)
            self.write_string(" " * (self.cols - (col - 1)), self.NO_CTRL)
            
        available = int(self.cols - (col - 1))
        self.set_ddram_addr(self.line_start_adr[row-1] + col - 1, self.NO_CTRL)
        text_to_write = str(str_data)
        self.write_string(text_to_write[0:available], self.NO_CTRL)

    def set_cursor(self, row: int, col: int):
        if row > self.rows: raise ValueError(f"Max row {self.rows}")
        if row < 3: self.enable_e1()
        else: self.enable_e2()
        self.set_ddram_addr(self.line_start_adr[row-1] + col - 1, self.NO_CTRL)

    def set_special_char(self, code: int, pattern: List[int], ctrl: int = HD44780.FIRST_CTRL):
        # CGRAM addressing is slightly different in AHW if we toggle
        self.set_cgram_addr(code * 8, self.NO_CTRL)
        wbuf1 = [0x05, 0x86] + pattern[:6]
        self.iow.write_report(1, wbuf1)
        wbuf2 = [0x05, 0x82] + pattern[6:8] + [0, 0, 0, 0]
        self.iow.write_report(1, wbuf2)
        
        self.toggle_enable()
        self.set_cgram_addr(code * 8, self.NO_CTRL)
        self.iow.write_report(1, wbuf1)
        self.iow.write_report(1, wbuf2)

    def set_iow_device(self, iow):
        super(HD44780, self).set_iow_device(iow)
        if iow:
            iow.set_direction(self.enable_port, 0) # Force output
            self.enable_e1()
            self.init_lcd(self.NO_CTRL)
            self.toggle_enable()
            self.init_lcd(self.NO_CTRL)

    def write_double_cmd(self, cmd: int):
        self.write_cmd(cmd, self.NO_CTRL)
        self.toggle_enable()
        self.write_cmd(cmd, self.NO_CTRL)

    def enable_e1(self):
        if self.current_enable != 1:
            self.iow.set_bit(self.enable_port, self.enable_bit)
            self.iow.write_io_ports()
            self.current_enable = 1

    def enable_e2(self):
        if self.current_enable != 2:
            self.iow.clear_bit(self.enable_port, self.enable_bit)
            self.iow.write_io_ports()
            self.current_enable = 2

    def toggle_enable(self):
        if self.current_enable == 1: self.enable_e2()
        else: self.enable_e1()

class LCD1x24(HD44780):
    def __init__(self):
        super().__init__()
        self.cols, self.rows, self.physical_rows = 24, 1, 1
        self.line_start_adr = [0x00]

class LCD2x16(HD44780):
    def __init__(self):
        super().__init__()
        self.cols, self.rows, self.physical_rows = 16, 2, 2
        self.line_start_adr = [0x00, 0x40]

class LCD2x20(HD44780):
    def __init__(self):
        super().__init__()
        self.cols, self.rows, self.physical_rows = 20, 2, 2
        self.line_start_adr = [0x00, 0x40]

class LCD2x24(HD44780):
    def __init__(self):
        super().__init__()
        self.cols, self.rows, self.physical_rows = 24, 2, 2
        self.line_start_adr = [0x00, 0x40]

class LCD2x40(HD44780):
    def __init__(self):
        super().__init__()
        self.cols, self.rows, self.physical_rows = 40, 2, 2
        self.line_start_adr = [0x00, 0x40]

class LCD4x16(HD44780):
    def __init__(self):
        super().__init__()
        self.cols, self.rows, self.physical_rows = 16, 4, 2
        self.line_start_adr = [0x00, 0x40, 0x10, 0x50]

class LCD4x20(HD44780):
    def __init__(self):
        super().__init__()
        self.cols, self.rows, self.physical_rows = 20, 4, 2
        self.line_start_adr = [0x00, 0x40, 0x14, 0x54]

class LCD4x27Ahw(DoubleHD44780Ahw):
    def __init__(self, enable_port: int, enable_bit: int):
        super().__init__(enable_port, enable_bit)
        self.cols, self.rows, self.physical_rows = 27, 4, 2
        self.line_start_adr = [0x00, 0x40, 0x00, 0x40]

class LCD4x27Iow56(DoubleHD44780Iow56):
    def __init__(self):
        super().__init__()
        self.cols, self.rows, self.physical_rows = 27, 4, 2
        self.line_start_adr = [0x00, 0x40, 0x00, 0x40]

class LCD4x40Ahw(DoubleHD44780Ahw):
    def __init__(self, enable_port: int, enable_bit: int):
        super().__init__(enable_port, enable_bit)
        self.cols, self.rows, self.physical_rows = 40, 4, 2
        self.line_start_adr = [0x00, 0x40, 0x00, 0x40]

class LCD4x40Iow56(DoubleHD44780Iow56):
    def __init__(self):
        super().__init__()
        self.cols, self.rows, self.physical_rows = 40, 4, 2
        self.line_start_adr = [0x00, 0x40, 0x00, 0x40]