import serial
import numpy as np
import regex as re
import audio_dspy as adsp
import matplotlib.pyplot as plt

from io import BytesIO
from pathlib import Path
from scipy import signal
from PIL import ImageChops, Image as im
from time import sleep

#TODO: Threading for read / write / serial

#==============================================================================

EQAPO = Path('C:\\Program Files\\EqualizerAPO\\config\\config.txt')
REW = Path('C:\\Program Files\\EqualizerAPO\\config\\17.01.24 20-20000 9db_max_boost.txt')

USE_EPAPER = True
COMPORT = 'COM8'

NBINS = 65536
FSAMPLE = 44100

#==============================================================================

rew_regex = r"Filter\s+([0-9]+):\s(?P<state>\w+)\s+(?P<type>\w+)\s+Fc\s+(?P<freq>\d+.\d+)\sHz\s+Gain\s+(?P<gain>-?\d+.\d+)\sdB\s+Q\s+(?P<qfactor>\d+.\d+)\n"
rew_pat = re.compile(rew_regex)

eq_apo_regex  = r"(?P<is_disabled>#?)\s?(?P<type>\w+):\s(?P<payload>[^\n]+)"
eq_apo_pat = re.compile(eq_apo_regex)


def read_rew_file(file):
    """Reads the Room EQ Wizard file and returns a list of dicts with the filter parameters.

    Args:
        file (str / path): Path to the Room EQ Wizard file.

    Returns:
        Combined frequency response of all REW filters in the file.
        x axis ticks for the frequency response.
    """
# read the room correction filter
    filterlist = []
    with open(file, 'r') as f:
        line = f.readline()
        while line:
            m = re.match(rew_pat, line)
            if m:
                filterlist.append(m.groupdict())
            line = f.readline()
            
    # read the applied eq filters   
    responselist = []
    # later combine with top loop
    for filter in filterlist:
        if filter['type'] == 'PK':
            #b,a = adsp.design_bell(float(filter['freq']), 1, float(filter['qfactor']), fs=FSAMPLE)
            b,a = signal.iirpeak(float(filter['freq']), float(filter['qfactor']), fs=FSAMPLE)
            w,h = signal.freqz(b, a, worN = NBINS ,fs=FSAMPLE)
            responselist.append((w,h,float(filter['gain'])))
            
    freq_ticks = w # frequency ticks of last filter (should be the same for all filters)
    
    room_corr_curve = np.zeros(NBINS) # initialize room correction curve

    for response in responselist:
        room_corr_curve += abs(response[1]) * response[2]
        
    return room_corr_curve, freq_ticks


def read_applied_eqs(path, freq_ticks):
    """Reads Equalizer APO config file and returns the added up frequency response of all EQs.

    Args:
        path (str / path): path to Equalizer APO config file. 

    Returns:
        Frequency response of all EQs in the config file.
        x axis ticks for the frequency response (same as for the REW curve)
    """
    eq_list = []
    with open(path, 'r') as f:
        line = f.readline()
        while line:
            m = re.match(eq_apo_pat, line)
            if m and m.group('is_disabled') != '#':
                eq_list.append(m.groupdict())

            line = f.readline()
        
        
    all_eq_comb = np.zeros(NBINS)

    for eq in eq_list:
        eq_f_gain_pairs = []
        eq_freq, eq_gain = [], []
        
        if eq['type'] == 'GraphicEQ':
            eq_f_gain_pairs = map(str.strip, eq['payload'].split(';')) # split at semicolon 
            eq_f_gain_pairs = map(str.split, eq_f_gain_pairs) # strip whitespaces
            #eq_curve = map(lambda x: (float(x[0]), float(x[1])), eq_curve) # convert to float

            for pair in eq_f_gain_pairs: eq_freq.append(float(pair[0])); eq_gain.append(float(pair[1]))

            eq_response = np.array(np.interp(freq_ticks, eq_freq, eq_gain) ) 
            
            all_eq_comb += eq_response
            
        elif eq['type'] == 'Filter':
            m = re.match(r"ON\s(?P<type>\w+)\s(?:(?P<steilheit>\d+)\sdB\s)?Fc\s(?P<freq>[\d\.]+)\sHz(?:\sGain\s(?P<gain>[\-\d\.]+)\s)?(?:dB\s)?(?:Q\s(?P<qfactor>[\-\d\.]+))?", eq['payload'])
            if m:
                typ = m.group('type')
                freq = float(m.group('freq'))
                
                gain = abs(float(m.group('gain'))) if m.group('gain') else None
                is_negative = True if m.group('gain') is not None and m.group('gain').startswith('-') else False
                
                q_factor = float(m.group('qfactor')) if m.group('qfactor') else 0.7
                
                # maybe switch case

                if typ in ['LSC', 'LS']: # low shelf

                    if is_negative:
                        #b,a = adsp.design_high_low_shelf(gain+10,10, freq, FSAMPLE)
                        #b,a = adsp.design_lowshelf(freq, q_factor, gain+1, FSAMPLE)
                        b,a = adsp.design_highshelf(freq, q_factor, gain+1, FSAMPLE)
                        w,h = signal.freqz(b, a, worN = NBINS ,fs=FSAMPLE)
                        #eq_response = (abs(h)*-1+1)
                        eq_response = abs(h)-gain-1
                    else:
                        #b,a = adsp.design_high_low_shelf(gain+1,1, freq, FSAMPLE)
                        b,a = adsp.design_lowshelf(freq, q_factor, gain+1, FSAMPLE)
                        w,h = signal.freqz(b, a, worN = NBINS ,fs=FSAMPLE)
                        eq_response = abs(h)-1


                elif typ in ['HS', 'HSC', 'LP', 'LPQ']: # high shelf
                    if typ in ['LP', 'LPQ']:
                        gain = 50
                        is_negative = True
                        
                    if is_negative:
                        b,a = adsp.design_lowshelf(freq, q_factor, gain+1, FSAMPLE)
                        w,h = signal.freqz(b, a, worN = NBINS ,fs=FSAMPLE)
                        eq_response = abs(h)-gain-1
                    else:
                        b,a = adsp.design_highshelf(freq, q_factor, gain+1, FSAMPLE)
                        w,h = signal.freqz(b, a, worN = NBINS ,fs=FSAMPLE)
                        #eq_response = (abs(h)*-1+1)
                        eq_response = abs(h)-1

                if eq_response is not None:
                    all_eq_comb += eq_response # superposition of all eqs
                    
                    
    return all_eq_comb

def create_bitmaps(freq_ticks, rew_curve, eq_curve):
    
    eqed_room_corr = rew_curve + eq_curve

    # Plot 
    DPI = 100
    fig_black = plt.figure(figsize=(296/DPI*296/229, 152/DPI*152/117), dpi = DPI) #Pixel/DPI -> inches | multiplication is bbox_inches='tight' workaround
    ax1 = fig_black.add_subplot(1, 1, 1)
    ax1.set_xscale('log')

    ax1.plot(freq_ticks, rew_curve-5, color='black', linewidth=1.0, linestyle='-', antialiased=False)
    ax1.plot(freq_ticks, eq_curve+15, color='gray', linewidth=1.0, linestyle='-', antialiased=False)
    #ax1.plot(freq_interval, combined*0.7, color='red', linewidth=1.0, linestyle='-', antialiased=False)
    ax1.fill_between(freq_ticks, rew_curve-5, eqed_room_corr-5, where=eqed_room_corr-5 <= rew_curve-0.5-5, facecolor='gray', antialiased=False)

    ax1.set_axis_off()
    ax1.margins(0, 0, tight=True)
    ax1.set_ylim([-30, 30])
    ax1.set_xlim([20, 20000])

    fig_red = plt.figure(figsize=(296/DPI*296/229, 152/DPI*152/117), dpi = DPI)
    ax2 = fig_red.add_subplot(1, 1, 1)
    ax2.set_xscale('log')
    ax2.margins(0, 0, tight=True)
    ax2.set_ylim([-30, 30])
    ax2.set_xlim([20, 20000])
    ax2.set_axis_off()

    ax2.fill_between(freq_ticks, rew_curve-5, eqed_room_corr-5, where=eqed_room_corr-5 >= rew_curve+0.5-5, facecolor='black', antialiased=False) #facecolor black but its the red bitmap, -5 for screen offset to bottom
    #ax2.fill_between(freq_ticks, room_corr_curve, eqed_room_corr, where=eqed_room_corr < room_corr_curve-0.5, facecolor='black', antialiased=False)


    # bytearray conversion
    buf = BytesIO()

    # save png to buffer
    fig_black.savefig(buf, format = "png", dpi=100, bbox_inches='tight', pad_inches=0.0)
    
    # open png image from buffer, convert to 1 byte depth
    buf.seek(0)
    image = im.open(buf).convert('1')
    image = image.rotate(90, expand = 1)

    black_bytes = np.array(image).tobytes()

    buf.truncate(0)
    buf.seek(0)

    fig_red.savefig(buf, format = "png", dpi=100, bbox_inches='tight', pad_inches=0.0)

    buf.seek(0)

    image = im.open(buf).convert('1')
    ImageChops.offset(image, 0, -3)
    image = image.rotate(90, expand = 1)
    red_bytes = np.array(image).tobytes()


    red_one_bit = []
    black_one_bit = []

    for i in range(0, len(black_bytes), 8):
        bin_list = [1 if x else 0 for x in black_bytes[i:i+8]]
        val = int(''.join(map(str, bin_list)), 2)
        black_one_bit.append(val)
        
    for i in range(0, len(red_bytes), 8):
        bin_list = [1 if x else 0 for x in red_bytes[i:i+8]]
        val = int(''.join(map(str, bin_list)), 2)
        red_one_bit.append(val)

    return black_one_bit, red_one_bit

            
def send_data(ser, bytes_black, bytes_red):
    """data_reworked_black = []
    data_reworked_red = []
    
    for b in bytes_black:
        if b == 0xff:
            data_reworked_black.append(0xfe)
        else:
            data_reworked_black.append(b)
    for b in bytes_red:
        if b == 0xff:
            data_reworked_red.append(0xfe)
        else:
            data_reworked_red.append(b)"""
                    
    #ser = serial.Serial(ser, 115200, timeout=1)

    ser.write(bytes_black + [0,0,0,0,0,0] + bytes_red + [0,0,0,0,0,0])
    #ser.write(bytes_red + [0,0,0,0,0,0])
    
    #read serial answewr until there is no more data
    x = b'1'
    while x != b'':
        x = ser.read()
        print(x)
        
    #close serial
    ser.flush()


def main():
    
    if USE_EPAPER:
        ser = serial.Serial(COMPORT, 115200, timeout=1, write_timeout=1,inter_byte_timeout=0.1)
        
        if not ser.isOpen():
            ser.open()
    
    room_rew_corr, freq_ticks = read_rew_file(REW)  
    all_eq_comb  = read_applied_eqs(EQAPO, freq_ticks)
    black, red = create_bitmaps(freq_ticks, room_rew_corr, all_eq_comb)
    
    send_data(ser, black, red)
    
    ser.flush()
    ser.close()


if __name__ == '__main__':
    #ser = serial.Serial(COMPORT, 115200, timeout=1)

    #ser.open()

    main()