# SPDX-License-Identifier: LGPL-3.0-or-later
# Copyright (C) 2018 Takashi Sakamoto

from threading import Thread
from struct import pack, unpack
from math import log10, pow

import gi
gi.require_version('GLib', '2.0')
gi.require_version('Hinawa', '3.0')
from gi.repository import GLib, Hinawa

from hinawa_utils.tscm.config_rom_parser import TscmConfigRomParser

__all__ = ['TscmUnit']


class TscmUnit(Hinawa.SndUnit):
    _BASE_ADDR = 0xffff00000000

    supported_sampling_rates = (44100, 48000, 88200, 96000)
    supported_clock_sources = ('Internal', 'Word-clock', 'S/PDIF', 'ADAT')
    supported_coax_sources = ('S/PDIF-1/2', 'Analog-1/2')
    supported_led_status = ('off', 'on')

    __SPECS = {
        'FW-1082': {
            'stream-in-ana-chs':    2,
            'has-opt-iface':        False,
        },
        'FW-1804': {
            'stream-in-ana-chs':    2,
            'has-opt-iface':        True,
        },
        'FW-1884': {
            'stream-in-ana-chs':    8,
            'has-opt-iface':        True,
        },
    }

    __MAX_THRESHOLD = 0x7fff

    def __init__(self, path):
        super().__init__()
        self.open(path)
        if self.get_property('type') != 6:
            raise ValueError('The character device is not for Tascam unit')

        ctx = GLib.MainContext.new()
        self.create_source().attach(ctx)
        self.__unit_dispatcher = GLib.MainLoop.new(ctx, False)
        self.__unit_th = Thread(target=lambda d: d.run(), args=(self.__unit_dispatcher, ))
        self.__unit_th.start()

        node = self.get_node()
        ctx = GLib.MainContext.new()
        node.create_source().attach(ctx)
        self.__node_dispatcher = GLib.MainLoop.new(ctx, False)
        self.__node_th = Thread(target=lambda d: d.run(), args=(self.__node_dispatcher, ))
        self.__node_th.start()

        parser = TscmConfigRomParser()
        info = parser.parse_rom(self.get_node().get_config_rom())
        self.model_name = info['model-name']
        self.__specs = self.__SPECS[self.model_name]

    def release(self):
        self.__unit_dispatcher.quit()
        self.__node_dispatcher.quit()
        self.__unit_th.join()
        self.__node_th.join()

    def __enter__(self):
        return self

    def __exit__(self, ex_type, ex_value, trace):
        self.release()

    def read_quadlet(self, offset):
        req = Hinawa.FwReq()
        frames = bytearray(4)
        return req.transaction(self.get_node(),
                        Hinawa.FwTcode.READ_QUADLET_REQUEST,
                        self._BASE_ADDR + offset, 4, frames)

    def write_quadlet(self, offset, frames):
        req = Hinawa.FwReq()
        return req.transaction(self.get_node(),
                        Hinawa.FwTcode.WRITE_QUADLET_REQUEST,
                        self._BASE_ADDR + offset, 4, frames)

    def get_firmware_versions(self):
        info = {}
        frames = self.read_quadlet(0x0000)
        info['Register'] = unpack('>I', frames)[0]
        frames = self.read_quadlet(0x0004)
        info['FPGA'] = unpack('>I', frames)[0]
        frames = self.read_quadlet(0x0008)
        info['ARM'] = unpack('>I', frames)[0]
        frames = self.read_quadlet(0x000c)
        info['HW'] = unpack('>I', frames)[0]
        return info

    def set_clock_source(self, src):
        if src not in self.supported_clock_sources:
            raise ValueError(
                'Invalid argument for clock source: {0}'.format(src))
        src = self.supported_clock_sources.index(src) + 1
        frames = self.read_quadlet(0x0228)
        frames = bytearray(frames)
        frames[0] = 0
        frames[1] = 0
        frames[3] = src
        self.write_quadlet(0x0228, frames)

    def get_clock_source(self):
        frames = self.read_quadlet(0x0228)
        index = frames[3] - 1
        if index < 0 or index >= len(self.supported_clock_sources):
            raise OSError('Unexpected value for clock source.')
        return self.supported_clock_sources[index]

    def set_sampling_rate(self, rate):
        if rate not in self.supported_sampling_rates:
            raise ValueError('Invalid argument for sampling rate.')
        if rate == 44100:
            flag = 0x01
        elif rate == 48000:
            flag = 0x02
        elif rate == 88200:
            flag = 0x81
        elif rate == 96000:
            flag = 0x82
        frames = self.read_quadlet(0x0228)
        frames = bytearray(frames)
        frames[3] = flag
        self.write_quadlet(0x0228, frames)

    def get_sampling_rate(self):
        frames = self.read_quadlet(0x0228)
        if (frames[1] & 0x0f) == 0x01:
            rate = 44100
        elif (frames[1] & 0x0f) == 0x02:
            rate = 48000
        else:
            raise OSError('Unexpected value for sampling rate.')
        if (frames[1] & 0xf0) == 0x80:
            rate *= 2
        elif (frames[1] & 0xf0) != 0x00:
            raise OSError('Unexpected value for sampling rate.')
        return rate

    def __set_flag(self, flag, mask):
        data = self.read_quadlet(0x022c)
        flags = data[3]
        data = bytearray(4)
        data[1] = (flags & ~flag) & mask    # removed bits.
        data[2] = (~flags & flag) & mask    # added bits.
        self.write_quadlet(0x022c, data)

    def get_stream_spdif_in_src_labels(self):
        labels = []
        labels.append('coax-iface')
        labels.append('opt-iface')
        return labels

    def set_stream_spdif_in_src(self, src):
        labels = self.get_stream_spdif_in_src_labels()
        if src not in labels:
            raise ValueError('Invalid argumen for source of stream spdif in.')
        flag = labels.index(src)
        self.__set_flag(flag, 0x01)

    def get_stream_spdif_in_src(self):
        labels = self.get_stream_spdif_in_src_labels()
        data = self.read_quadlet(0x022c)
        index = data[3] & 0x01
        return labels[index]

    def get_coax_out_src_labels(self):
        labels = []
        ana_ch = self.__specs['stream-in-ana-chs']
        labels.append('stream-in-{0}/{1}'.format(ana_ch + 1, ana_ch + 2))
        labels.append('analog-out-1/2')
        return labels

    def set_coax_out_src(self, src):
        labels = self.get_coax_out_src_labels()
        if src not in labels:
            raise ValueError('Invalid argument for source of coax out iface.')
        flag = labels.index(src) << 1
        self.__set_flag(flag, 0x02)

    def get_coax_out_src(self):
        labels = self.get_coax_out_src_labels()
        data = self.read_quadlet(0x022c)
        index = (data[3] & 0x02) >> 1
        return labels[index]

    def get_opt_out_src_labels(self):
        labels = []
        if self.__specs['has-opt-iface']:
            ana_ch = self.__specs['stream-in-ana-chs']

            label = 'stream-in-{0}:{1}'.format(ana_ch + 1, ana_ch + 9)
            labels.append(label)

            label = 'stream-in-{0}/{1}'.format(ana_ch + 10, ana_ch + 11)
            labels.append(label)

            labels.append('analog-in-1:8')
            if ana_ch > 2:
                labels.append('analog-out-1:8')

        return labels

    def set_opt_out_src(self, src):
        labels = self.get_opt_out_src_labels()
        if src not in labels:
            print('Invalid argument for source of opt out iface.')
        flag = labels.index(src) << 2
        if flag == 0x0c:
            flag = 0x88
        self.__set_flag(flag, 0x8c)

    def get_opt_out_src(self):
        labels = self.get_opt_out_src_labels()
        data = self.read_quadlet(0x022c)
        index = (data[3] & 0x0c) >> 2
        if data[3] & 0x80:
            index += 1
        print(index)
        return labels[index]

    def set_input_threshold(self, level):
        data = self.read_quadlet(0x0230)
        data = bytearray(data)
        if level == float('-inf'):
            val = 0x0000
        else:
            val = int(self.__MAX_THRESHOLD * pow(10, level / 20))
        chunks = pack('>H', val)
        data[0] = chunks[0]
        data[1] = chunks[1]
        self.write_quadlet(0x0230, data)

    def get_input_threshold(self):
        data = self.read_quadlet(0x0230)
        val = unpack('>H', data[0:2])[0]
        if val == 0x0000:
            return float('-inf')
        return 20 * log10(val / self.__MAX_THRESHOLD)
