#!/usr/bin/env python3
# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019.
# Built in San Francisco.

# This software is distributed under a commercial license for personal,
# educational, corporate or any other use.
# The software as a whole or any parts of it is prohibited for distribution or
# use without obtaining a license from 3D Control Systems, Inc.

# All software licenses are subject to the 3DPrinterOS terms of use
# (available at https://www.3dprinteros.com/terms-and-conditions/),
# and privacy policy (available at https://www.3dprinteros.com/privacy-policy/)

import logging
import os
import platform
import subprocess


# flake8 --config=tox.ini gpio_map.py
# pycodestyle --config=tox.ini gpio_map.py


class GPIOMap:

    # GPIO pin map to CPU pin
    RPI_GPIO_PINS    = [None,   None,
                       2,      None,
                       3,      None,
                       4,      14,
                       None,   15,
                       17,     18,
                       27,     None,
                       22,     23,
                       None,   24,
                       10,     None,
                       9,      25,
                       11,     8,
                       None,   7,
                       None,   None,
                       5,      None,
                       6,      12,
                       13,     None,
                       19,     16,
                       26,     20,
                       None,   21]

    ROCK3A_GPIO_PINS = [None,   None,
                       32,     None,
                       33,     None,
                       111,    25,
                       None,   24,
                       116,    99,
                       117,    None,
                       16,     97,
                       17,     106,
                       147,    None,
                       149,    None,
                       146,    150,
                       None,   153,
                       14,     13,
                       95,     None,
                       96,     114,
                       115,    None,
                       100,    98,
                       112,    102,
                       None,   101]

    # ROCK4_GPIO_PINS = [None,   None,
    #                    32,     None,
    #                    33,     None,
    #                    111,    25,
    #                    None,   24,
    #                    116,    99,
    #                    117,    None,
    #                    16,     97,
    #                    17,     106,
    #                    147,    None,
    #                    149,    None,
    #                    146,    150,
    #                    None,   153,
    #                    14,     13,
    #                    95,     None,
    #                    96,     114,
    #                    115,    None,
    #                    100,    98,
    #                    112,    102,
    #                    None,   101]

    OPI_ZERO_GPIO_PINS = [None,   None,
                       12,     None,
                       11,     None,
                       6,      198,
                       None,   199,
                       1,      7,
                       0,      None,
                       3,      19,
                       None,   18,
                       15,     None,
                       16,     2,
                       14,     13,
                       None,   10]


    RADXA_GPIO_PINS  = [None,   None,
                       490,    None,
                       491,    None,
                       415,    412,
                       None,   413,
                       414,    501,
                       503,    None,
                       None,   502,
                       None,   500,
                       447,    None,
                       448,    475,
                       450,    449,
                       None,   None,
                       415,    414,
                       None,   None,
                       None,   416,
                       None,   None,
                       420,    451,
                       421,    422,
                       None,   423]

    RPI_GPIO_CONFIG        = {'map':RPI_GPIO_PINS, 'chip':{'0':range(0, 57), '1':range(58, 66)}, 'offset': 0}
    RPI5_GPIO_CONFIG        = {'map':RPI_GPIO_PINS, 'chip':{'4':range(0, 57)}, 'offset': 0}
    ROCK3A_GPIO_CONFIG     = {'map':ROCK3A_GPIO_PINS, 'chip':{'0':range(0, 31), '1':range(32, 63), '2':range(64, 95), '3':range(96, 127), '4':range(128, 160)}, 'offset': 0}
    # ROCK4_GPIO_CONFIG     = {'map':ROCK4_GPIO_PINS, 'chip':{'0':range(0, 31), '1':range(32, 63), '2':range(64, 95), '3':range(96, 127), '4':range(128, 160)}, 'offset':0}
    OPI_ZERO_GPIO_CONFIG   = {'map':OPI_ZERO_GPIO_PINS, 'chip':{'0':range(0, 223), '1':range(224, 256)}, 'offset':0}
    RAXDA_GPIO_CONFIG      = {'map':RADXA_GPIO_PINS, 'chip':{'1':range(0, 14), '0':range(15, 100)}, 'offset':412}

    PER_BOARD_GPIO_CONFIGS = {"Raspberry Pi 5": RPI5_GPIO_CONFIG,
              "Raspberry Pi": RPI_GPIO_CONFIG,
              "Radxa ROCK3 Model A": ROCK3A_GPIO_CONFIG,
              # "Radxa ROCK4": ROCK4_GPIO_CONFIG,
              "Xunlong Orange Pi Zero": OPI_ZERO_GPIO_CONFIG,
              "Radxa Zero": RAXDA_GPIO_CONFIG}

    MODEL_NAME_FILE = "/sys/firmware/devicetree/base/model"

    @staticmethod
    def get_machine_model_name():
        platform_info = platform.platform()
        if 'arm' in platform_info or 'aarch64' in platform_info:
            try:
                if os.path.exists(GPIOMap.MODEL_NAME_FILE):
                    with open(GPIOMap.MODEL_NAME_FILE) as f:
                        model_name = f.read()
                        if model_name:
                            return model_name
                        raise ValueError
            except (OSError, TypeError, ValueError):
                try:
                    return subprocess.check_output(r"dmesg | grep 'Machine model' | sed -r 's/(^.+)(Machine model:\s+)(.+)/\3/g'", shell=True, text=True).strip()
                except:
                    pass

    def __init__(self):
        self.logger = logging.getLogger(self.__class__.__name__)
        self.machine_name = self.get_machine_model_name()
        self.gpio = None
        if not self.machine_name:
            error = "Unable to detect machine name. Assuming no GPIO interface."
            self.logger.error(error)
        else:
            for board_name in self.PER_BOARD_GPIO_CONFIGS:
                if board_name in self.machine_name:
                    self.gpio = self.PER_BOARD_GPIO_CONFIGS[board_name]
                    self.logger.info(f"{self.machine_name} board detected")
                    break
            else:
                self.logger.error(f"No supported board detected. Supported Boards list: {self.PER_BOARD_GPIO_CONFIGS.keys()}.\n Detected: {self.machine_name}")

    def is_gpio_detected(self):
        return bool(self.gpio)

    def get_chip_line(self, gpio_pin):
        if self.is_gpio_detected():
            try:
                return self.gpio['map'][gpio_pin - 1]
            except IndexError:
                self.logger.warning(f'No such pin number: {gpio_pin}')
            except TypeError:
                self.logger.warning(f'Not GPIO pin number: {gpio_pin}')
            except KeyError:
                self.logger.warning(f"Board map for {self.machine_name} is incorrect")
            except Exception as e:
                self.logger.warning(f'Error map pin {gpio_pin}\n{e}')

    def get_cgpio_dev_and_line(self, gpio_pin):
        if self.is_gpio_detected():
            try:
                cpu_pin = self.get_chip_line(gpio_pin)
                cpu_pin_offset = cpu_pin - self.gpio['offset']
                assert cpu_pin_offset >= 0, f"CPU pin offset #{gpio_pin} = {cpu_pin_offset} < 0 must greater or equal 0 board map or offset({self.gpio['offset']}) for {self.machine_name} is incorrect\n{self.gpio}"
                chip_offset = 0
                for chip_number in list(self.gpio['chip']):
                    if cpu_pin_offset in self.gpio['chip'][chip_number]:
                        return ("/dev/gpiochip" + chip_number, cpu_pin_offset - chip_offset)
                    if self.gpio['offset'] != 0:
                        chip_offset += len(self.gpio['chip'][chip_number])
            except IndexError:
                self.logger.warning(f'No such pin number: {gpio_pin}')
            except TypeError:
                self.logger.warning(f'Not GPIO pin number: {gpio_pin}')
            except KeyError:
                self.logger.warning(f"Board map for {self.machine_name} is incorrect\n{self.gpio}")
            except AssertionError as e:
                self.logger.error(e)
            except Exception as e:
                self.logger.warning(f'Error map pin {gpio_pin}\n{e}')


def _dbg(_wrapper):
    print("######################## DEBUG list all pins ########################")
    for i in range(1, len(_wrapper.gpio['map']) + 1):
        desc = _wrapper.get_cgpio_dev_and_line(i)
        if desc is not None:
            print(i, _wrapper.get_cgpio_dev_and_line(i))


if __name__ == "__main__":
    wrapper = GPIOMap()
    _dbg(wrapper)
    for board_name in GPIOMap.PER_BOARD_GPIO_CONFIGS:
        wrapper.gpio = GPIOMap.PER_BOARD_GPIO_CONFIGS[board_name]
        _dbg(wrapper)
