# The MIT License (MIT)
#
# Copyright (c) 2017 Brent Rubell for Adafruit Industries
# Copyright (c) 2018 Kattni Rembor for Adafruit Industries
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
`adafruit_character_lcd.character_lcd`
====================================================

Module for interfacing with monochromatic character LCDs

* Author(s): Kattni Rembor, Brent Rubell, Asher Lieber,
  Tony DiCola (original python charLCD library)

Implementation Notes
--------------------

**Hardware:**

"* `Adafruit Character LCDs <http://www.adafruit.com/category/63_96>`_"

**Software and Dependencies:**

* Adafruit CircuitPython firmware:
  https://github.com/adafruit/circuitpython/releases
* Adafruit's Bus Device library (when using I2C/SPI):
  https://github.com/adafruit/Adafruit_CircuitPython_BusDevice

"""

import time
import digitalio
from micropython import const

__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_CharLCD.git"

# pylint: disable-msg=bad-whitespace
# Commands
_LCD_CLEARDISPLAY = const(0x01)
_LCD_RETURNHOME = const(0x02)
_LCD_ENTRYMODESET = const(0x04)
_LCD_DISPLAYCONTROL = const(0x08)
_LCD_CURSORSHIFT = const(0x10)
_LCD_FUNCTIONSET = const(0x20)
_LCD_SETCGRAMADDR = const(0x40)
_LCD_SETDDRAMADDR = const(0x80)

# Entry flags
_LCD_ENTRYLEFT = const(0x02)
_LCD_ENTRYSHIFTDECREMENT = const(0x00)

# Control flags
_LCD_DISPLAYON = const(0x04)
_LCD_CURSORON = const(0x02)
_LCD_CURSOROFF = const(0x00)
_LCD_BLINKON = const(0x01)
_LCD_BLINKOFF = const(0x00)

# Move flags
_LCD_DISPLAYMOVE = const(0x08)
_LCD_MOVERIGHT = const(0x04)
_LCD_MOVELEFT = const(0x00)

# Function set flags
_LCD_4BITMODE = const(0x00)
_LCD_2LINE = const(0x08)
_LCD_1LINE = const(0x00)
_LCD_5X8DOTS = const(0x00)

# Offset for up to 4 rows.
_LCD_ROW_OFFSETS = (0x00, 0x40, 0x14, 0x54)

# pylint: enable-msg=bad-whitespace


def _set_bit(byte_value, position, val):
    # Given the specified byte_value set the bit at position to the provided
    # boolean value val and return the modified byte.
    ret = None
    if val:
        ret = byte_value | (1 << position)
    else:
        ret = byte_value & ~(1 << position)
    return ret


def _map(xval, in_min, in_max, out_min, out_max):
    # Affine transfer/map with constrained output.
    outrange = float(out_max - out_min)
    inrange = float(in_max - in_min)
    ret = (xval - in_min) * (outrange / inrange) + out_min
    if out_max > out_min:
        ret = max(min(ret, out_max), out_min)
    else:
        ret = max(min(ret, out_min), out_max)
    return ret


# pylint: disable-msg=too-many-instance-attributes
class Character_LCD:
    """Base class for character LCD.

    :param ~digitalio.DigitalInOut rs: The reset data line
    :param ~digitalio.DigitalInOut en: The enable data line
    :param ~digitalio.DigitalInOut d4: The data line 4
    :param ~digitalio.DigitalInOut d5: The data line 5
    :param ~digitalio.DigitalInOut d6: The data line 6
    :param ~digitalio.DigitalInOut d7: The data line 7
    :param columns: The columns on the charLCD
    :param lines: The lines on the charLCD

    """

    LEFT_TO_RIGHT = const(0)
    RIGHT_TO_LEFT = const(1)

    # pylint: disable-msg=too-many-arguments
    def __init__(self, rs, en, d4, d5, d6, d7, columns, lines):

        self.columns = columns
        self.lines = lines
        #  save pin numbers
        self.reset = rs
        self.enable = en
        self.dl4 = d4
        self.dl5 = d5
        self.dl6 = d6
        self.dl7 = d7

        # set all pins as outputs
        for pin in (rs, en, d4, d5, d6, d7):
            pin.direction = digitalio.Direction.OUTPUT

        # Initialise the display
        self._write8(0x33)
        self._write8(0x32)
        # Initialise display control
        self.displaycontrol = _LCD_DISPLAYON | _LCD_CURSOROFF | _LCD_BLINKOFF
        # Initialise display function
        self.displayfunction = _LCD_4BITMODE | _LCD_1LINE | _LCD_2LINE | _LCD_5X8DOTS
        # Initialise display mode
        self.displaymode = _LCD_ENTRYLEFT | _LCD_ENTRYSHIFTDECREMENT
        # Write to displaycontrol
        self._write8(_LCD_DISPLAYCONTROL | self.displaycontrol)
        # Write to displayfunction
        self._write8(_LCD_FUNCTIONSET | self.displayfunction)
        # Set entry mode
        self._write8(_LCD_ENTRYMODESET | self.displaymode)
        self.clear()

        self._message = None
        self._enable = None
        self._direction = None
        # track row and column used in cursor_position
        # initialize to 0,0
        self.row = 0
        self.column = 0
        self._column_align = False

    # pylint: enable-msg=too-many-arguments

    def home(self):
        """Moves the cursor "home" to position (1, 1)."""
        self._write8(_LCD_RETURNHOME)
        time.sleep(0.003)

    def clear(self):
        """Clears everything displayed on the LCD.

        The following example displays, "Hello, world!", then clears the LCD.

        .. code-block:: python

            import time
            import board
            import busio
            import adafruit_character_lcd.character_lcd_i2c as character_lcd

            i2c = busio.I2C(board.SCL, board.SDA)
            lcd = character_lcd.Character_LCD_I2C(i2c, 16, 2)

            lcd.message = "Hello, world!"
            time.sleep(5)
            lcd.clear()
        """
        self._write8(_LCD_CLEARDISPLAY)
        time.sleep(0.003)

    @property
    def column_align(self):
        """If True, message text after '\\n' starts directly below start of first
        character in message. If False, text after '\\n' starts at column zero.
        """
        return self._column_align

    @column_align.setter
    def column_align(self, enable):
        if isinstance(enable, bool):
            self._column_align = enable
        else:
            raise ValueError("The column_align value must be either True or False")

    @property
    def cursor(self):
        """True if cursor is visible. False to stop displaying the cursor.

        The following example shows the cursor after a displayed message:

        .. code-block:: python

            import time
            import board
            import busio
            import adafruit_character_lcd.character_lcd_i2c as character_lcd

            i2c = busio.I2C(board.SCL, board.SDA)
            lcd = character_lcd.Character_LCD_I2C(i2c, 16, 2)

            lcd.cursor = True
            lcd.message = "Cursor! "
            time.sleep(5)

        """
        return self.displaycontrol & _LCD_CURSORON == _LCD_CURSORON

    @cursor.setter
    def cursor(self, show):
        if show:
            self.displaycontrol |= _LCD_CURSORON
        else:
            self.displaycontrol &= ~_LCD_CURSORON
        self._write8(_LCD_DISPLAYCONTROL | self.displaycontrol)

    def cursor_position(self, column, row):
        """Move the cursor to position ``column``, ``row`` for the next
        message only. Displaying a message resets the cursor position to (0, 0).

            :param column: column location
            :param row: row location
        """
        # Clamp row to the last row of the display
        if row >= self.lines:
            row = self.lines - 1
        # Clamp to last column of display
        if column >= self.columns:
            column = self.columns - 1
        # Set location
        self._write8(_LCD_SETDDRAMADDR | (column + _LCD_ROW_OFFSETS[row]))
        # Update self.row and self.column to match setter
        self.row = row
        self.column = column

    @property
    def blink(self):
        """
        Blink the cursor. True to blink the cursor. False to stop blinking.

        The following example shows a message followed by a blinking cursor for five seconds.

        .. code-block:: python

            import time
            import board
            import busio
            import adafruit_character_lcd.character_lcd_i2c as character_lcd

            i2c = busio.I2C(board.SCL, board.SDA)
            lcd = character_lcd.Character_LCD_I2C(i2c, 16, 2)

            lcd.blink = True
            lcd.message = "Blinky cursor!"
            time.sleep(5)
            lcd.blink = False
        """
        return self.displaycontrol & _LCD_BLINKON == _LCD_BLINKON

    @blink.setter
    def blink(self, blink):
        if blink:
            self.displaycontrol |= _LCD_BLINKON
        else:
            self.displaycontrol &= ~_LCD_BLINKON
        self._write8(_LCD_DISPLAYCONTROL | self.displaycontrol)

    @property
    def display(self):
        """
        Enable or disable the display. True to enable the display. False to disable the display.

        The following example displays, "Hello, world!" on the LCD and then turns the display off.

        .. code-block:: python

            import time
            import board
            import busio
            import adafruit_character_lcd.character_lcd_i2c as character_lcd

            i2c = busio.I2C(board.SCL, board.SDA)
            lcd = character_lcd.Character_LCD_I2C(i2c, 16, 2)

            lcd.message = "Hello, world!"
            time.sleep(5)
            lcd.display = False
        """
        return self.displaycontrol & _LCD_DISPLAYON == _LCD_DISPLAYON

    @display.setter
    def display(self, enable):
        if enable:
            self.displaycontrol |= _LCD_DISPLAYON
        else:
            self.displaycontrol &= ~_LCD_DISPLAYON
        self._write8(_LCD_DISPLAYCONTROL | self.displaycontrol)

    @property
    def message(self):
        """Display a string of text on the character LCD.
        Start position is (0,0) if cursor_position is not set.
        If cursor_position is set, message starts at the set
        position from the left for left to right text and from
        the right for right to left text. Resets cursor column
        and row to (0,0) after displaying the message.

        The following example displays, "Hello, world!" on the LCD.

        .. code-block:: python

            import time
            import board
            import busio
            import adafruit_character_lcd.character_lcd_i2c as character_lcd

            i2c = busio.I2C(board.SCL, board.SDA)
            lcd = character_lcd.Character_LCD_I2C(i2c, 16, 2)

            lcd.message = "Hello, world!"
            time.sleep(5)
        """
        return self._message

    @message.setter
    def message(self, message):
        self._message = message
        # Set line to match self.row from cursor_position()
        line = self.row
        # Track times through iteration, to act on the initial character of the message
        initial_character = 0
        # iterate through each character
        for character in message:
            # If this is the first character in the string:
            if initial_character == 0:
                # Start at (0, 0) unless direction is set right to left, in which case start
                # on the opposite side of the display if cursor_position not set or (0,0)
                # If cursor_position is set then starts at the specified location for
                # LEFT_TO_RIGHT. If RIGHT_TO_LEFT cursor_position is determined from right.
                # allows for cursor_position to work in RIGHT_TO_LEFT mode
                if self.displaymode & _LCD_ENTRYLEFT > 0:
                    col = self.column
                else:
                    col = self.columns - 1 - self.column
                self.cursor_position(col, line)
                initial_character += 1
            # If character is \n, go to next line
            if character == "\n":
                line += 1
                # Start the second line at (0, 1) unless direction is set right to left in
                # which case start on the opposite side of the display if cursor_position
                # is (0,0) or not set. Start second line at same column as first line when
                # cursor_position is set
                if self.displaymode & _LCD_ENTRYLEFT > 0:
                    col = self.column * self._column_align
                else:
                    if self._column_align:
                        col = self.column
                    else:
                        col = self.columns - 1
                self.cursor_position(col, line)
            # Write string to display
            else:
                self._write8(ord(character), True)
        # reset column and row to (0,0) after message is displayed
        self.column, self.row = 0, 0

    def move_left(self):
        """Moves displayed text left one column.

        The following example scrolls a message to the left off the screen.

        .. code-block:: python

            import time
            import board
            import busio
            import adafruit_character_lcd.character_lcd_i2c as character_lcd

            i2c = busio.I2C(board.SCL, board.SDA)
            lcd = character_lcd.Character_LCD_I2C(i2c, 16, 2)

            scroll_message = "<-- Scroll"
            lcd.message = scroll_message
            time.sleep(2)
            for i in range(len(scroll_message)):
                lcd.move_left()
                time.sleep(0.5)
        """
        self._write8(_LCD_CURSORSHIFT | _LCD_DISPLAYMOVE | _LCD_MOVELEFT)

    def move_right(self):
        """Moves displayed text right one column.

        The following example scrolls a message to the right off the screen.

        .. code-block:: python

            import time
            import board
            import busio
            import adafruit_character_lcd.character_lcd_i2c as character_lcd

            i2c = busio.I2C(board.SCL, board.SDA)
            lcd = character_lcd.Character_LCD_I2C(i2c, 16, 2)

            scroll_message = "Scroll -->"
            lcd.message = scroll_message
            time.sleep(2)
            for i in range(len(scroll_message) + 16):
                lcd.move_right()
                time.sleep(0.5)
        """
        self._write8(_LCD_CURSORSHIFT | _LCD_DISPLAYMOVE | _LCD_MOVERIGHT)

    @property
    def text_direction(self):
        """The direction the text is displayed. To display the text left to right beginning on the
        left side of the LCD, set ``text_direction = LEFT_TO_RIGHT``. To display the text right
        to left beginning on the right size of the LCD, set ``text_direction = RIGHT_TO_LEFT``.
        Text defaults to displaying from left to right.

        The following example displays "Hello, world!" from right to left.

        .. code-block:: python

            import time
            import board
            import busio
            import adafruit_character_lcd.character_lcd_i2c as character_lcd

            i2c = busio.I2C(board.SCL, board.SDA)
            lcd = character_lcd.Character_LCD_I2C(i2c, 16, 2)

            lcd.text_direction = lcd.RIGHT_TO_LEFT
            lcd.message = "Hello, world!"
            time.sleep(5)
        """
        return self._direction

    @text_direction.setter
    def text_direction(self, direction):
        self._direction = direction
        if direction == self.LEFT_TO_RIGHT:
            self._left_to_right()
        elif direction == self.RIGHT_TO_LEFT:
            self._right_to_left()

    def _left_to_right(self):
        # Displays text from left to right on the LCD.
        self.displaymode |= _LCD_ENTRYLEFT
        self._write8(_LCD_ENTRYMODESET | self.displaymode)

    def _right_to_left(self):
        # Displays text from right to left on the LCD.
        self.displaymode &= ~_LCD_ENTRYLEFT
        self._write8(_LCD_ENTRYMODESET | self.displaymode)

    def create_char(self, location, pattern):
        """
        Fill one of the first 8 CGRAM locations with custom characters.
        The location parameter should be between 0 and 7 and pattern should
        provide an array of 8 bytes containing the pattern. E.g. you can easily
        design your custom character at http://www.quinapalus.com/hd44780udg.html
        To show your custom character use, for example, ``lcd.message = "\x01"``

        :param location: integer in range(8) to store the created character
        :param ~bytes pattern: len(8) describes created character

        """
        # only position 0..7 are allowed
        location &= 0x7
        self._write8(_LCD_SETCGRAMADDR | (location << 3))
        for i in range(8):
            self._write8(pattern[i], char_mode=True)

    def _write8(self, value, char_mode=False):
        # Sends 8b ``value`` in ``char_mode``.
        # :param value: bytes
        # :param char_mode: character/data mode selector. False (default) for
        # data only, True for character bits.
        #  one ms delay to prevent writing too quickly.
        time.sleep(0.001)
        #  set character/data bit. (charmode = False)
        self.reset.value = char_mode
        # WRITE upper 4 bits
        self.dl4.value = ((value >> 4) & 1) > 0
        self.dl5.value = ((value >> 5) & 1) > 0
        self.dl6.value = ((value >> 6) & 1) > 0
        self.dl7.value = ((value >> 7) & 1) > 0
        #  send command
        self._pulse_enable()
        # WRITE lower 4 bits
        self.dl4.value = (value & 1) > 0
        self.dl5.value = ((value >> 1) & 1) > 0
        self.dl6.value = ((value >> 2) & 1) > 0
        self.dl7.value = ((value >> 3) & 1) > 0
        self._pulse_enable()

    def _pulse_enable(self):
        # Pulses (lo->hi->lo) to send commands.
        self.enable.value = False
        # 1microsec pause
        time.sleep(0.0000001)
        self.enable.value = True
        time.sleep(0.0000001)
        self.enable.value = False
        time.sleep(0.0000001)


# pylint: enable-msg=too-many-instance-attributes


# pylint: disable-msg=too-many-instance-attributes
class Character_LCD_Mono(Character_LCD):
    """Interfaces with monochromatic character LCDs.

        :param ~digitalio.DigitalInOut rs: The reset data line
        :param ~digitalio.DigitalInOut en: The enable data line
        :param ~digitalio.DigitalInOut d4: The data line 4
        :param ~digitalio.DigitalInOut d5: The data line 5
        :param ~digitalio.DigitalInOut d6: The data line 6
        :param ~digitalio.DigitalInOut d7: The data line 7
        :param columns: The columns on the charLCD
        :param lines: The lines on the charLCD
        :param ~digitalio.DigitalInOut backlight_pin: The backlight pin
        :param bool backlight_inverted: ``False`` if LCD is not inverted, i.e. backlight pin is
            connected to common anode. ``True`` if LCD is inverted i.e. backlight pin is connected
            to common cathode.

    """

    # pylint: disable-msg=too-many-arguments
    def __init__(
        self,
        rs,
        en,
        db4,
        db5,
        db6,
        db7,
        columns,
        lines,
        backlight_pin=None,
        backlight_inverted=False,
    ):

        # Backlight pin and inversion
        self.backlight_pin = backlight_pin
        self.backlight_inverted = backlight_inverted

        #  Setup backlight
        if backlight_pin is not None:
            self.backlight_pin.direction = digitalio.Direction.OUTPUT
            self.backlight = True
        super().__init__(rs, en, db4, db5, db6, db7, columns, lines)

    # pylint: enable-msg=too-many-arguments

    @property
    def backlight(self):
        """Enable or disable backlight. True if backlight is on. False if backlight is off.

        The following example turns the backlight off, then displays, "Hello, world?", then turns
        the backlight on and displays, "Hello, world!"

        .. code-block:: python

            import time
            import board
            import busio
            import adafruit_character_lcd.character_lcd_i2c as character_lcd

            i2c = busio.I2C(board.SCL, board.SDA)

            lcd = character_lcd.Character_LCD_I2C(i2c, 16, 2)

            lcd.backlight = False
            lcd.message = "Hello, world?"
            time.sleep(5)
            lcd.backlight = True
            lcd.message = "Hello, world!"
            time.sleep(5)

        """
        return self._enable

    @backlight.setter
    def backlight(self, enable):
        self._enable = enable
        if enable:
            self.backlight_pin.value = not self.backlight_inverted
        else:
            self.backlight_pin.value = self.backlight_inverted


class Character_LCD_RGB(Character_LCD):
    """Interfaces with RGB character LCDs.

        :param ~digitalio.DigitalInOut rs: The reset data line
        :param ~digitalio.DigitalInOut en: The enable data line
        :param ~digitalio.DigitalInOut db4: The data line 4
        :param ~digitalio.DigitalInOut db5: The data line 5
        :param ~digitalio.DigitalInOut db6: The data line 6
        :param ~digitalio.DigitalInOut db7: The data line 7
        :param columns: The columns on the charLCD
        :param lines: The lines on the charLCD
        :param ~pulseio.PWMOut, ~digitalio.DigitalInOut red: Red RGB Anode
        :param ~pulseio.PWMOut, ~digitalio.DigitalInOut green: Green RGB Anode
        :param ~pulseio.PWMOut, ~digitalio.DigitalInOut blue: Blue RGB Anode
        :param ~digitalio.DigitalInOut read_write: The rw pin. Determines whether to read to or
            write from the display. Not necessary if only writing to the display. Used on shield.

    """

    # pylint: disable-msg=too-many-arguments
    def __init__(
        self,
        rs,
        en,
        db4,
        db5,
        db6,
        db7,
        columns,
        lines,
        red,
        green,
        blue,
        read_write=None,
    ):

        # Define read_write (rw) pin
        self.read_write = read_write

        # Setup rw pin if used
        if read_write is not None:
            self.read_write.direction = digitalio.Direction.OUTPUT

        # define color params
        self.rgb_led = [red, green, blue]

        for pin in self.rgb_led:
            if hasattr(pin, "direction"):
                # Assume a digitalio.DigitalInOut or compatible interface:
                pin.direction = digitalio.Direction.OUTPUT
            elif not hasattr(pin, "duty_cycle"):
                raise TypeError(
                    "RGB LED objects must be instances of digitalio.DigitalInOut"
                    " or pulseio.PWMOut, or provide a compatible interface."
                )

        self._color = [0, 0, 0]
        super().__init__(rs, en, db4, db5, db6, db7, columns, lines)

    @property
    def color(self):
        """
        The color of the display. Provide a list of three integers ranging 0 - 100, ``[R, G, B]``.
        ``0`` is no color, or "off". ``100`` is maximum color. For example, the brightest red would
        be ``[100, 0, 0]``, and a half-bright purple would be, ``[50, 0, 50]``.

        If PWM is unavailable, ``0`` is off, and non-zero is on. For example, ``[1, 0, 0]`` would
        be red.

        The following example turns the LCD red and displays, "Hello, world!".

        .. code-block:: python

            import time
            import board
            import busio
            import adafruit_character_lcd.character_lcd_rgb_i2c as character_lcd

            i2c = busio.I2C(board.SCL, board.SDA)

            lcd = character_lcd.Character_LCD_RGB_I2C(i2c, 16, 2)

            lcd.color = [100, 0, 0]
            lcd.message = "Hello, world!"
            time.sleep(5)
        """
        return self._color

    @color.setter
    def color(self, color):
        self._color = color
        for number, pin in enumerate(self.rgb_led):
            if hasattr(pin, "duty_cycle"):
                # Assume a pulseio.PWMOut or compatible interface and set duty cycle:
                pin.duty_cycle = int(_map(color[number], 0, 100, 65535, 0))
            elif hasattr(pin, "value"):
                # If we don't have a PWM interface, all we can do is turn each color
                # on / off.  Assume a DigitalInOut (or compatible interface) and write
                # 0 (on) to pin for any value greater than 0, or 1 (off) for 0:
                pin.value = not color[number] > 1
