#!/usr/bin/env python

#Copyright (c) 2017 James Gibbard

#Plot an IQ data file based on provided parameters
#Tested with python 2.7 and 3.6
#Requires numpy and matplotlib

import argparse
from sys import byteorder
import numpy as np
import matplotlib.pyplot as plt

def plotIQ(data, Fs):
    
    
    if Fs == None:
        plt.plot(np.real(data), label='I')
        plt.plot(np.imag(data), label='Q')
    else:
        plt.plot(np.real(data), label='I')
        plt.plot(np.imag(data), label='Q')
        
    plt.grid(True)  
    plt.legend(loc='upper right', frameon=True)        
    plt.show()
    
def plotPSD(data,fftWindow, Fs):

    assert fftWindow in ['rectangular', 'bartlett', 'blackman', 
                         'hamming', 'hanning']
    
    N = len(data)
    
    #Generate the selected window
    if fftWindow == "rectangular":
        window = np.ones(N)
    elif fftWindow == "bartlett":
        window = np.bartlett(N)
    elif args.fftWindow == "blackman":
        window = np.blackman(N)
    elif fftWindow == "hamming":
        window = np.hamming(N)
    elif fftWindow == "hanning":
         window = np.hanning(N)         
         
    dft = np.fft.fft(data*window)    
    
    if Fs == None:
        #If the sample rate is known then plot PSD as
        #Power/Freq in (dB/Hz)
        plt.psd(data*window, NFFT=N)
        
    else:
        #If sample rate is not known then plot PSD as
        #Power/Freq as (dB/rad/sample)
        plt.psd(data*window, NFFT=N, Fs=Fs)

    plt.show()
    
def plotSpectrogram(data, fftWindow, fftSize, Fs):

    if fftSize == None:
        N = len(data)
    else:
        N = fftSize    
    
    if Fs == None:
        Fs = 2
    
    if fftWindow == "rectangular":
        plt.specgram(data, NFFT=N, Fs=Fs, 
        window=lambda data: data*np.ones(len(data)),  noverlap=int(N/10))
    elif fftWindow == "bartlett":
        plt.specgram(data, NFFT=N, Fs=Fs, 
        window=lambda data: data*np.bartlett(len(data)),  noverlap=int(N/10))
    elif args.fftWindow == "blackman":
        plt.specgram(data, NFFT=N, Fs=Fs, 
        window=lambda data: data*np.blackman(len(data)),  noverlap=int(N/10))
    elif fftWindow == "hamming":
        plt.specgram(data, NFFT=N, Fs=Fs, 
        window=lambda data: data*np.hamming(len(data)),  noverlap=int(N/10))
    elif fftWindow == "hanning":
         plt.specgram(data, NFFT=N, Fs=Fs, 
         window=lambda data: data*np.hanning(len(data)),  noverlap=int(N/10))

    plt.show()
    

if __name__ == '__main__':
    
    #Generate command line parser to parse inputs
    cliParser = argparse.ArgumentParser(description='Plots quadrature IQ signals')    
    
    #Get the filename of the input file
    cliParser.add_argument('filename', type=str, help='input filename')
    
    cliParser.add_argument('-s', '--startSample', type=int, 
        help='sample to begin plot from (default=0)',  default=0)    

    cliParser.add_argument('-o', '--offset', type=int, 
        help='offset in bytes from begining of file (default=0)',  default=0)

    cliParser.add_argument('-n', '--numberOfSamples', type=int, 
        help='number of samples to plot',  default=0)       

    cliParser.add_argument('-fs', '--sampleRate', type=float, 
        help='sets the sample rate [sps] (default=1e6)')    
    
    cliParser.add_argument('-f', '--format', type=str, 
        help='Output format (default=int16)', 
        choices=["int8", "int16", "int32", "uint8", "uint16", "uint32", 
        "float16", "float32", "float64"],
        default='int16')
                                
    cliParser.add_argument('-be', '--bigendian', action='store_true', 
        help='output data in big endian format (default=False)')
                        
    cliParser.add_argument('-qi', '--orderQI', action='store_true', 
        help='store output data as Q then I (Default = I then Q)')

    cliParser.add_argument('-p', '--plotType', type=str, 
        help='Plot Type (default=iq)', choices=['iq', 'psd', 'spec'],
        default='iq')

    cliParser.add_argument('-w', '--fftWindow', type=str, 
        help='FFT window type (default=rectangular)', 
        choices=['rectangular', 'bartlett', 'blackman', 'hamming', 'hanning'],
        default='rectangular')    

    cliParser.add_argument('-fw', '--fftWidth', type=int, 
        help='FFT width for spectrogram')

    args = cliParser.parse_args()
    
    #By default the file is read from an offset of 0 bytes
    fileOffset = 0
    
    #Set initial offset in bytes
    #Useful if the file has a header that should be ignored
    if args.offset != 0:
        fileOffset = args.offset
        
    #Convert sample offset to offset in bytes depending on datatype
    if args.startSample != 0:
        if args.format[-1:] == "8":
            fileOffset += 2 * 1 * args.startSample
        elif args.format[-2:] == "16":
            fileOffset += 2 * 2 * args.startSample 
        elif args.format[-2:] == "32":
            fileOffset += 2 * 4 * args.startSample 
        elif args.format[-2:] == "64":
            fileOffset += 2 * 8 * args.startSample 
    
    #Open the file in binary read mode    
    with open(args.filename, "rb") as f:
    
        #Seek to the absolue offset set by offset and startSample arguments
        f.seek(fileOffset, 0)
        
        if args.numberOfSamples != 0:
            #Read twice the number of samples as each sample has a 
            #real and imaginary part
            data = np.fromfile(f, dtype=args.format, 
            count=args.numberOfSamples*2)
        else:
            #If number of samples is not specified read to the end of the file
            data = np.fromfile(f, dtype=args.format)
    
    #If system byteorder is different to desired input byte order
    #Then swich the endianness
    if byteorder == 'little':
        if args.bigendian == True:        
            data = data.byteswap()            
    elif byteorder == 'big':
        if args.bigendian == False:     
            data = data.byteswap()    
    
    #Convert to complex data to complex128 data type
    #This is two double precision (64bit) floating point numbers
    if args.orderQI:
        data = data[1::2] + 1j * data[0::2]
    else:
        data = data[0::2] + 1j * data[1::2]
    
    #Select plotting function
    if args.plotType == 'iq':
        plotIQ(data, args.sampleRate)
    elif args.plotType == 'psd':
        plotPSD(data, args.fftWindow, args.sampleRate)
    elif args.plotType == 'spec':
        plotSpectrogram(data, args.fftWindow, args.fftWidth, args.sampleRate)