#!/usr/bin/env python3
# -*- coding: utf-8 -*-

#**************************************************************************
#   Copyright (C) 2018, Paul Lutus                                        *
#                                                                         *
#   This program is free software; you can redistribute it and/or modify  *
#   it under the terms of the GNU General Public License as published by  *
#   the Free Software Foundation; either version 2 of the License, or     *
#   (at your option) any later version.                                   *
#                                                                         *
#   This program is distributed in the hope that it will be useful,       *
#   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
#   GNU General Public License for more details.                          *
#                                                                         *
#   You should have received a copy of the GNU General Public License     *
#   along with this program; if not, write to the                         *
#   Free Software Foundation, Inc.,                                       *
#   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
#**************************************************************************

import re
import sys
import os
import time
import struct
import signal
import numpy as np

import osmosdr

from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5.QtWidgets import QWidget

from gnuradio import analog
from gnuradio import audio
from gnuradio import blocks
from gnuradio import filter
from gnuradio import gr
from gnuradio.filter import firdes
from gnuradio.fft import logpwrfft

import sip

class DrawGraphics(QtCore.QObject):
    draw = QtCore.pyqtSignal() 

# a convenience class to acquire data from Gnuradio
    
class MyVectorSink(gr.sync_block):
  def __init__(self,main,sz):
    self.main = main
    self.sz = sz
     
    gr.sync_block.__init__(
    self,
    name = "My Vector sink",
    in_sig = [(np.float32,self.sz)],
    out_sig = None,
    )
    # event-related
    self.drawgr = DrawGraphics()
    self.drawgr.draw.connect(self.main.draw_fft_disp)

  def work(self, input_items, output_items):
    if(self.main.graphic_data == None):
      data = np.fft.fftshift(input_items)
      self.main.graphic_data = data[0][0].tolist()
      self.drawgr.draw.emit()
    return len(input_items)
     
class Radio(gr.top_block,QWidget):
  def __init__(self,main):
    self.main = main
    gr.top_block.__init__(self, "Top Block")
    QWidget.__init__(self)    
    self.fir_offset_f = 0
    self.cw_offset = 0
    self.blocks_multiply_const_volume = None
    self.logpwrfft = None
    self.audio_sink = None
    self.osmosdr_source = None
    self.analog_agc_cc = None
    self.analog_pwr_squelch = None
    self.analog_pwr_squelch_ssb = None
    self.freq_xlating_fir_filter = None
    self.low_pass_filter_am = None
    self.low_pass_filter_fm = None
    self.low_pass_filter_wfm = None
    self.low_pass_filter_ssb = None
    self.band_pass_filter_cw = None
    self.cw_base = None
    self.mode = None
    self.sample_rate = None
    self.audio_rate = None
    self.gain_names = None
    self.device_found = False
    self.currently_configured_device = None
    self.error = False
    #self.if_offset_f = 0
    
  def ntrp(self,x,xa,xb,ya,yb):
    return (x-xa) * (yb-ya) / (xb-xa) + ya
    
  def initialize_radio(self,config,run = False):
    # intermediate frequency constants
    self.if_sample_rate = int(240e3)
    self.ssb_hi = 3000
    self.ssb_lo = 100
    self.hilbert_taps_ssb = 128
    self.cw_base = config['cw_base']
    self.cw_lo = -self.cw_base/2
    self.cw_hi = self.cw_base/2
    #print("*** entering initialize radio")
    self.mode = self.main.mode_control.get_index()
    #print("cw_base: %d" % self.cw_base)
    self.cw_offset = 0
    #self.sample_rate = self.main.sample_rate_control.get_value()
    self.audio_rate = self.main.audio_rate_control.get_value()
    self.main.offset_freq_control.set_range(self.audio_rate/2)
    self.squelch_level = self.main.squelch_control.get_value()
    self.update_offset_values()
    self.device_name = self.main.device_control.get_value()
    self.device_driver_name = self.main.device_dict[self.device_name]
    self.configure_source_controls()
    
        
    #if self.main.full_rebuild_flag or self.error:
    self.build_blocks(config)
    if not self.error:
      self.connect_blocks(config)
    self.main.full_rebuild_flag = False
  
  def limit_offset_range(self,a,b):
    f = abs(a)
    sign = (-1,1)[a >= 0]
    f = (f,b)[f > b]
    return f * sign
    
  def update_offset_values(self):
    if self.mode != None:
      self.offset_mode = self.main.offset_state_control.get_value()
      if self.offset_mode:
        f = self.main.offset_freq_control.get_value()
        self.fir_offset_f = self.limit_offset_range(f,self.audio_rate/2)
      else:
        self.fir_offset_f = 0
        
  def change_antennas(self,value):
    if self.osmosdr_source != None:
      self.osmosdr_source.set_antenna(value)
      #print("changed to antenna: %s" % value)
          
  def test_set_cw_offset(self):
    offset = 0
    if self.mode == self.main.MODE_CW_LSB or self.mode == self.main.MODE_CW_USB:
      if self.mode == self.main.MODE_CW_LSB:
        offset = -self.cw_base/2
      else:
        offset = self.cw_base/2
    return offset
     
  def compute_offset_f(self,front_end = True):
    if self.audio_rate != None:
      self.fir_offset_f = self.limit_offset_range(self.fir_offset_f,self.audio_rate/2)
    if front_end:
      return self.fir_offset_f - self.cw_offset
    else:
      return -(self.fir_offset_f + self.cw_offset)
  
  def update_freq_xlating_fir_filter(self):
    if self.freq_xlating_fir_filter != None:
      f = self.compute_offset_f(False)
      self.freq_xlating_fir_filter.set_center_freq(f)
    
  # changing between USB and LSB requires changing the sign of a multiplication term  
  def create_usb_lsb_switch(self):
    USB = self.mode == self.main.MODE_USB or self.mode == self.main.MODE_CW_USB
    self.blocks_multiply_const_ssb = blocks.multiply_const_vff(((1,-1)[USB], ))

  def create_update_freq_xlating_fir_filter(self):
    if self.sample_rate != None:
      self.update_offset_values()
      if self.mode == self.main.MODE_WFM:
        rate = self.if_sample_rate
      else:
        rate = self.audio_rate
        
      fir_taps = firdes.complex_band_pass(1, rate, -rate/2, rate/2,rate/2)
      if self.freq_xlating_fir_filter == None:
        self.freq_xlating_fir_filter = filter.freq_xlating_fir_filter_ccc(1, (fir_taps), self.compute_offset_f(False), rate)
      else:
        self.freq_xlating_fir_filter.set_taps(fir_taps)
        self.freq_xlating_fir_filter.set_center_freq(self.compute_offset_f(False))
  
  def rebuild_filters(self,config,value = None):
    if self.cw_base == None:
      return
    if value == None:
      value = config['bw_mode']
      
    am_bw = (8000,3000,2000)[value]
    fm_bw = (8000,6000,4000)[value]
    wfm_bw = (60e3,40e3,20e3)[value]
    ssb_bw = (5000,2400,1800)[value]
    cw_bw = (self.cw_base*2/3,self.cw_base/2,self.cw_base/3)[value]
    
    am_taps = firdes.low_pass(
      1, self.audio_rate, am_bw, 500, firdes.WIN_HAMMING, 6.76)
    fm_taps = firdes.low_pass(
      1, self.audio_rate, fm_bw, 500, firdes.WIN_HAMMING, 6.76)
    wfm_taps = firdes.low_pass(
      1, self.if_sample_rate, wfm_bw, 4e3, firdes.WIN_HAMMING, 6.76)
    ssb_taps = firdes.low_pass(
      1, self.audio_rate, ssb_bw, 100, firdes.WIN_HAMMING, 6.76)
    #print("CW Base: %d - %d - %d" % (self.cw_base-cw_bw,self.cw_base + cw_bw,self.audio_rate))
    cw_taps = firdes.band_pass(
      1, self.audio_rate, self.cw_base-cw_bw,self.cw_base+cw_bw, 100, firdes.WIN_HAMMING, 6.76)
    
    if self.low_pass_filter_am == None:
      self.low_pass_filter_am = filter.fir_filter_ccf(1, am_taps)
    else:
      self.low_pass_filter_am.set_taps(am_taps) 
      
    if self.low_pass_filter_fm == None:
      self.low_pass_filter_fm = filter.fir_filter_ccf(1, fm_taps)
    else:
      self.low_pass_filter_fm.set_taps(fm_taps)
      
    if self.low_pass_filter_wfm == None:
      self.low_pass_filter_wfm = filter.fir_filter_ccf(1, wfm_taps)
    else:
      self.low_pass_filter_wfm.set_taps(wfm_taps)
      
    if self.low_pass_filter_ssb == None:
      self.low_pass_filter_ssb = filter.fir_filter_fff(1, ssb_taps)
    else:
      self.low_pass_filter_ssb.set_taps(ssb_taps)
      
    if self.band_pass_filter_cw == None:
      self.band_pass_filter_cw = filter.fir_filter_fff(1, cw_taps)
    else:
      self.band_pass_filter_cw.set_taps(cw_taps)
      
  # calculate gcd using Euclid's algorithm
  def gcd(self,a, b):
    while b:
        a, b = b, a%b
    return a
    
  def compute_dec_interp(self,a,b):
    a = int(a)
    b = int(b)
    gcd = self.gcd(a,b)
    dec = a/gcd
    interp = b/gcd
    return dec,interp
    
  # reference at  https://github.com/osmocom/gr-osmosdr/blob/master/include/osmosdr/source.h

  def configure_source_controls(self):
    if self.osmosdr_source == None or self.device_driver_name != self.currently_configured_device:
      self.osmosdr_source = osmosdr.source( args="numchan=1 %s" % self.device_driver_name)
      self.currently_configured_device = self.device_driver_name
    
    # this is required to allow a change in bandwidth
    self.osmosdr_source.set_bandwidth(1)
    
    self.gain_names = self.osmosdr_source.get_gain_names()
    if len(self.gain_names) == 0:
      # no device found
      self.main.run_stop_button.setEnabled(False)
      self.device_found = False
    else:
      self.device_found = True
      self.main.set_agc_mode()
      self.main.corr_ppm_control.set_value()
      self.main.dc_offset_control.set_value()
      self.main.iq_balance_control.set_value()
      self.main.run_stop_button.setEnabled(True)
      antennas = self.osmosdr_source.get_antennas()
      self.main.antenna_control.set_content(antennas)
      self.main.antenna_control.set_value()
      
    rng = self.osmosdr_source.get_bandwidth_range().values()
    if len(rng) == 0:
      self.bandwidth_range = ["%d" % 10**x for x in range(3,9,1)]
    else:
      self.bandwidth_range = ["%d" % x for x in rng]
    self.main.bandwidth_control.set_content(self.bandwidth_range)
    self.main.bandwidth_control.enable(True)
    self.main.bandwidth_label.setText("RF BW Hz")
    self.main.bandwidth_control.set_value()
    
    rng = self.osmosdr_source.get_sample_rates().values()
    if len(rng) == 0:
      rates = [int(x*10e6) for x in range(1,24,1)]
    else:
      rates = [x for x in rng]
    all_rates = []
    # add some slower rates not provided by the device
    # example HackRF will operate at 1 MHz
    # but only provides rates >= 8 MHz
    for n in range(1,10,1):
      r = n*10**6
      if r >= rates[0]: break
      all_rates.append(r)
    all_rates += rates
    self.sample_rates = ["%d" % x for x in all_rates]
    self.main.sample_rate_control.set_content(self.sample_rates)
    self.main.sample_rate_control.enable(True)
    self.sample_rate = self.main.sample_rate_control.get_value()
    if self.device_found:
      self.osmosdr_source.set_sample_rate(self.sample_rate)
      
    controls = [
      self.main.gain_control_a,
      self.main.gain_control_b,
      self.main.gain_control_c,
      self.main.gain_control_d
    ]
    labels = [
      self.main.gain_label_a,
      self.main.gain_label_b,
      self.main.gain_label_c,
      self.main.gain_label_d
    ]
      
    for n,control in enumerate(controls):
      if n >= len(self.gain_names):
        # not in use or out of range
        control.visible(False)
        labels[n].setVisible(False)
      else:
        # activate and set range
        control.visible(True)
        name = self.gain_names[n]
        control.set_gain_name(name)
        labels[n].setText("%s Gain" % name)
        labels[n].setVisible(True)
        grange = self.osmosdr_source.get_gain_range(name).values()
        # get the beginning and end values
        a,b = grange[1],grange[-1]
        control.set_range(a,b)
        control.set_value()
           
  # initial setup
  def build_blocks(self,config):
    if not self.device_found:
      return
      
    self.error = False

    fft_size = self.main.fft_size_control.get_value()
    
    frame_rate = self.main.framerate_control.get_value()
    average = self.main.average_control.get_value()
    ssb_lo = self.ssb_lo
    ssb_hi = self.ssb_hi
    
    USB = self.mode == self.main.MODE_USB or self.mode == self.main.MODE_CW_USB
      
    self.audio_dec_nrw = 1
    
    self.dec_nrw, self.interp_nrw = self.compute_dec_interp(self.sample_rate,self.audio_rate)
    
    self.audio_dec_wid = self.if_sample_rate / self.audio_rate
    
    self.dec_wid, self.interp_wid = self.compute_dec_interp(self.sample_rate,self.if_sample_rate)
          
    volume = .1
    
    self.configure_source_controls()
    
    self.create_update_freq_xlating_fir_filter()
    
    self.analog_agc_cc = analog.agc2_cc(1e-1, 1e-2, 1.0, 1.0)
    self.analog_agc_cc.set_max_gain(1)
    
    self.analog_agc_ff = analog.agc2_ff(1e-1, 1e-2, 1.0, 1.0)
    self.analog_agc_ff.set_max_gain(1)
        
    self.rational_resampler_wid = filter.rational_resampler_ccc(
      decimation=int(self.dec_wid),
      interpolation=int(self.interp_wid),
      taps=None,
      fractional_bw=None,
        )
        
    self.rational_resampler_nrw = filter.rational_resampler_ccc(
      decimation=int(self.dec_nrw),
      interpolation=int(self.interp_nrw),
      taps=None,
      fractional_bw=None,
        )
        
    self.analog_pwr_squelch = analog.pwr_squelch_cc(self.squelch_level, 1e-4, 0, True)
    
    self.analog_pwr_squelch_ssb = analog.pwr_squelch_ff(self.squelch_level, 1e-4, 0, True)
        
    self.blocks_multiply = blocks.multiply_vcc(1)
    self.blocks_complex_to_real = blocks.complex_to_real(1)
    
    #self.rebuild_filters(config)
     
    self.blocks_complex_to_mag_am = blocks.complex_to_mag(1)
      
    self.analog_nbfm_rcv = analog.nbfm_rx(
        audio_rate=self.audio_rate,
        quad_rate=self.audio_rate,
        tau=75e-6,
        max_dev=6e3,
        )
      
    self.analog_wfm_rcv = analog.wfm_rcv(
        quad_rate=self.if_sample_rate,
        audio_decimation=self.audio_dec_wid,
      )
      
    self.hilbert_fc_2 = filter.hilbert_fc(self.hilbert_taps_ssb, firdes.WIN_HAMMING, 6.76)
    self.hilbert_fc_1 = filter.hilbert_fc(self.hilbert_taps_ssb, firdes.WIN_HAMMING, 6.76)
    
    self.blocks_multiply_ssb = blocks.multiply_vcc(1)
    
    self.blocks_complex_to_float_ssb = blocks.complex_to_float(1)
    
    self.create_usb_lsb_switch()
    
    self.blocks_add = blocks.add_vff(1)
    
    self.blocks_complex_to_real = blocks.complex_to_real(1)
    self.blocks_complex_to_imag = blocks.complex_to_imag(1)
         
    # this is the source for the FFT display's data  
    self.logpwrfft = logpwrfft.logpwrfft_c(
      sample_rate=self.sample_rate,
      fft_size=fft_size,
      ref_scale=2,
      frame_rate=frame_rate,
      avg_alpha=average,
      average=(average != 1),
        )

    # this is the main FFT display
    self.fft_vector_sink = MyVectorSink(self.main,fft_size)
    
    self.blocks_multiply_const_volume = blocks.multiply_const_vff((volume, ))
        
    # only create this once
    if self.audio_sink == None:
      try:
        self.audio_sink = audio.sink(self.audio_rate, config['audio_device'], True)
      except Exception as e:
        self.main.message_dialog("Audio Error","A problem has come up while accessing the audio system: %s" % e)
        self.error = True
        self.audio_sink = None

    self.main.af_gain_control.set_value()
       
  def connect_blocks(self,config):
    self.disconnect_all()
    
    if (self.error == True) or (self.device_found == False):
      return
    
    self.cw_offset = self.test_set_cw_offset()
    self.main.set_agc_mode()
    self.rebuild_filters(config)
    
    self.connect((self.osmosdr_source, 0), (self.logpwrfft, 0))
    self.connect((self.logpwrfft, 0), (self.fft_vector_sink, 0))

    if self.mode == self.main.MODE_AM:
      self.connect((self.osmosdr_source, 0), (self.rational_resampler_nrw, 0))
      self.connect((self.rational_resampler_nrw, 0), (self.freq_xlating_fir_filter, 0))
      self.connect((self.freq_xlating_fir_filter, 0), (self.low_pass_filter_am, 0))
      self.connect((self.low_pass_filter_am, 0), (self.analog_pwr_squelch, 0))
      self.connect((self.analog_pwr_squelch, 0), (self.analog_agc_cc, 0))
      self.connect((self.analog_agc_cc, 0), (self.blocks_complex_to_mag_am, 0))
      self.connect((self.blocks_complex_to_mag_am, 0), (self.blocks_multiply_const_volume, 0))
      self.connect((self.blocks_multiply_const_volume, 0), (self.audio_sink, 0))

    elif self.mode == self.main.MODE_FM:
      self.connect((self.osmosdr_source, 0), (self.rational_resampler_nrw, 0))
      self.connect((self.rational_resampler_nrw, 0), (self.freq_xlating_fir_filter, 0))
      self.connect((self.freq_xlating_fir_filter, 0), (self.low_pass_filter_fm, 0))
      self.connect((self.low_pass_filter_fm, 0), (self.analog_pwr_squelch, 0))
      self.connect((self.analog_pwr_squelch, 0), (self.analog_agc_cc, 0))
      self.connect((self.analog_agc_cc, 0), (self.analog_nbfm_rcv, 0))
      self.connect((self.analog_nbfm_rcv, 0), (self.blocks_multiply_const_volume, 0))
      self.connect((self.blocks_multiply_const_volume, 0), (self.audio_sink, 0))
     
    elif self.mode == self.main.MODE_WFM:
      self.connect((self.osmosdr_source, 0), (self.rational_resampler_wid, 0))
      self.connect((self.rational_resampler_wid, 0), (self.freq_xlating_fir_filter, 0))
      self.connect((self.freq_xlating_fir_filter, 0), (self.low_pass_filter_wfm, 0))
      self.connect((self.low_pass_filter_wfm, 0), (self.analog_pwr_squelch, 0))
      self.connect((self.analog_pwr_squelch, 0), (self.analog_agc_cc, 0))
      self.connect((self.analog_agc_cc, 0), (self.analog_wfm_rcv, 0)) 
      self.connect((self.analog_wfm_rcv, 0), (self.blocks_multiply_const_volume, 0))
      self.connect((self.blocks_multiply_const_volume, 0), (self.audio_sink, 0))
      
    elif self.mode == self.main.MODE_USB or self.mode == self.main.MODE_LSB:
      self.create_usb_lsb_switch()
      self.connect((self.osmosdr_source, 0), (self.rational_resampler_nrw, 0))
      self.connect((self.rational_resampler_nrw, 0), (self.freq_xlating_fir_filter, 0))
      self.connect((self.freq_xlating_fir_filter, 0), (self.analog_pwr_squelch, 0))
      self.connect((self.analog_pwr_squelch, 0), (self.blocks_complex_to_float_ssb, 0))
      self.connect((self.blocks_complex_to_float_ssb, 0), (self.hilbert_fc_1, 0))
      self.connect((self.blocks_complex_to_float_ssb, 1), (self.hilbert_fc_2, 0))
      self.connect((self.hilbert_fc_1, 0), (self.blocks_complex_to_real, 0))
      self.connect((self.hilbert_fc_2, 0), (self.blocks_complex_to_imag, 0))
      self.connect((self.blocks_complex_to_imag, 0), (self.blocks_multiply_const_ssb, 0))
      self.connect((self.blocks_multiply_const_ssb, 0), (self.blocks_add, 1))
      self.connect((self.blocks_complex_to_real, 0), (self.blocks_add, 0))
      self.connect((self.blocks_add, 0), (self.low_pass_filter_ssb, 0))
      self.connect((self.low_pass_filter_ssb, 0), (self.analog_pwr_squelch_ssb, 0))
      self.connect((self.analog_pwr_squelch_ssb, 0), (self.analog_agc_ff, 0))
      self.connect((self.analog_agc_ff, 0), (self.blocks_multiply_const_volume, 0))
      self.connect((self.blocks_multiply_const_volume, 0), (self.audio_sink, 0))
        
    elif self.mode == self.main.MODE_CW_USB or self.mode == self.main.MODE_CW_LSB:
      self.create_usb_lsb_switch()
      self.connect((self.osmosdr_source, 0), (self.rational_resampler_nrw, 0))
      self.connect((self.rational_resampler_nrw, 0), (self.freq_xlating_fir_filter, 0))
      self.connect((self.freq_xlating_fir_filter, 0), (self.analog_pwr_squelch, 0))
      self.connect((self.analog_pwr_squelch, 0), (self.blocks_complex_to_float_ssb, 0))
      self.connect((self.blocks_complex_to_float_ssb, 0), (self.hilbert_fc_1, 0))
      self.connect((self.blocks_complex_to_float_ssb, 1), (self.hilbert_fc_2, 0))
      self.connect((self.hilbert_fc_1, 0), (self.blocks_complex_to_real, 0))
      self.connect((self.hilbert_fc_2, 0), (self.blocks_complex_to_imag, 0))
      self.connect((self.blocks_complex_to_imag, 0), (self.blocks_multiply_const_ssb, 0))
      self.connect((self.blocks_multiply_const_ssb, 0), (self.blocks_add, 1))
      self.connect((self.blocks_complex_to_real, 0), (self.blocks_add, 0))
      self.connect((self.blocks_add, 0), (self.band_pass_filter_cw, 0))
      self.connect((self.band_pass_filter_cw, 0), (self.analog_pwr_squelch_ssb, 0))
      self.connect((self.analog_pwr_squelch_ssb, 0), (self.analog_agc_ff, 0))
      self.connect((self.analog_agc_ff, 0), (self.blocks_multiply_const_volume, 0))

      self.connect((self.blocks_multiply_const_volume, 0), (self.audio_sink, 0))

    else:
      print("mode error -- no recognizable mode selected.")