#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Apr 20 2020

@author: JKH

Inspired in part by bruker_opus_filereader.py by Thomas Wagner
https://bitbucket.org/hirschbeutel/ono/src/default/ono/bruker_opus_filereader.py
and BrukerOpusReader by tdyczek: https://github.com/qedsoftware/brukeropusreader
and also GGG (including OpusHdr)

Returns a dictionary where the keys are for the different OPUS file blocks.

"""
import struct
import numpy as np
import copy





def read_opus_file(opus_fname):
    # example input:
#    opus_fname = '/home/osboxes/research/tccon/egi2020/example_data/ifgs/20140613/cn20140613.ifg.0175'
#    opus_fname = '/home/osboxes/research/tccon/egi2020/example_data/inst_data/xd/20180716/xd20180716.ifg.1078'


  #%% hard-coded info about blocks in a dictionary
  # tcd= 'text', 'channel', or 'data'
  block_dict = {0: {'tcd': 't', 8: 'Info Block', 104: 'Datafile History', 152: 'Curve Fit', 
                      168: 'Signature', 240: 'Integration Method'},
                7: {'tcd': 'c', 4: 'ScSm', 8: 'IgSm', 12: 'PhSm', 132: 'ScSm/2.Chn.', 136: 'IgSm/2.Chn.'}, 
                11: {'tcd': 'c', 4: 'ScRf', 8: 'IgRf'}, 
                15: 'AB',
                23: {'tcd': 'c', 4: 'Data Parameters ScSm', 8: 'Data Parameters IgSm', 12: 'Data Parameters PhSm', 
                       132: 'Data Parameters ScSm/2.Chn.', 136: 'Data Parameters IgSm/2.Chn.'},
                27: {'tcd': 'c', 4: 'Data Parameter ScRf', 8: 'Data Parameter IgRf'}, 
                31: 'AB Data Parameter',
                32: 'Instrument Parameters', 
                40: 'Instrument (Rf)', 
                48: 'Acquisition Parameters', 
                56: 'Acquisition (Rf)', 
                64: 'FT - Parameters', 
                72: 'FT (Rf)', 
                96: 'Optic Parameters', 
                104: 'Optic (Rf)', 
                160: 'Sample'}
  # 23:  0x84 => 'SpSm/2.Chn.',
  # 0x88 => 'IgSm/2.Chn.'  136
  
  
  #0:[8:param, 104:text]
  #7-15:series
  #23+:param
  blk_params = {0: "int", 1: "float", 2: "str", 3: "str", 4: "str"} #types in parameter description blocks
  
  
  
  
  #%% read in file all at once
  with open(opus_fname, 'rb') as fid: #open for binary reading
    raw_data = fid.read()
  
  nBytes = len(raw_data)
  
  
  #%% Parse out header. Header sections provide details about the different chunks/blocks
  nHdr = 504 #hard coded...
  hdr_dat = raw_data[0:nHdr]
  rPos = 24 #current read position
  blk_sz = 12 #
  
  
  blck_dat = {'offset': [], 'chunkSize': [], 'datType': [], 'channel': [], 
              'textType': []} #pre-allocate lists
  while rPos + blk_sz <= nHdr:
    
    offset = struct.unpack('<I', hdr_dat[rPos+8:rPos+12])[0]
    blck_dat['offset'].append(offset)
    
    if offset <= 0:
      break
      
    else:
      blk_prm = struct.unpack('<B', hdr_dat[rPos:rPos+1])[0]
      blck_dat['datType'].append(int(blk_prm)) #also convert to integer
      
      blk_prm = struct.unpack('<B', hdr_dat[rPos+1:rPos+2])[0]
      blck_dat['channel'].append(int(blk_prm))
      
      blk_prm = struct.unpack('<B', hdr_dat[rPos+2:rPos+3])[0]
      blck_dat['textType'].append(int(blk_prm))
      
      chunk_size = struct.unpack('<I', hdr_dat[rPos+4:rPos+8])[0]
      blck_dat['chunkSize'].append(chunk_size)
      
      next_offset = offset + 4 * chunk_size
      if next_offset >= nBytes: #no more chunks in file
        #(This can happen even when not to end of blocks in the header)
        break
      
      rPos += blk_sz
      
  
  #%%   
  out_data = {} #pre-allocate as dictionary
  
  for iBlk in range(len(blck_dat['datType'])):
    if blck_dat['datType'][iBlk] in block_dict:
      bi_sub = block_dict[blck_dat['datType'][iBlk]]
    else:
      out_data[blck_dat['datType'][iBlk]] = 'This block type is not recognized yet.'
    
    ###### Get the block name
    if type(bi_sub) == dict:
      try:
        if bi_sub['tcd'] == 't':
          bi_name = bi_sub[ blck_dat['textType'][iBlk] ]
        elif bi_sub['tcd'] == 'c':
          bi_name = bi_sub[ blck_dat['channel'][iBlk] ]
      except:
        out_data['unknown ' + '%d' % blck_dat['offset'][iBlk] ] = 'Unknown block. Type: %d  Offset: %d  Length: %d  Channel: %d  TextType: %d' % \
                      (blck_dat['datType'][iBlk], blck_dat['offset'][iBlk], blck_dat['chunkSize'][iBlk], blck_dat['channel'][iBlk], blck_dat['textType'][iBlk])   
        continue
    else:
      bi_name = bi_sub
    
    ###### Decide if the block is a parameter, data, or text
    # (Might be able to replace hard-coded specifications with try statements??)
    ptd = 'p' #parse as parameter, data, or text
    if blck_dat['datType'][iBlk]>=7 and blck_dat['datType'][iBlk]<=15:
      ptd='d'
    elif blck_dat['datType'][iBlk]==0:
      ptd='t'
      if blck_dat['textType'][iBlk] == 8:
        ptd='p'
    
    
    ##### parse and save data
    cnkS = blck_dat['offset'][iBlk]
    cnkE = cnkS + blck_dat['chunkSize'][iBlk]*4
    chunk = raw_data[cnkS:cnkE]
    if ptd == 't': #text block
      # convert bytes to text, remove leading characters
      out_data[bi_name] = chunk.decode("latin-1").strip("\x00") #
      
    elif ptd == 'd': #data block
      bin_fmt = f'<' + '%d'%blck_dat['chunkSize'][iBlk] + 'f' #binary data format
      out_data[bi_name] = np.array(  struct.unpack(bin_fmt, chunk)  )  #unpack data, save to numpy array
    
    
    
    elif ptd == 'p': #parameter block
      rPos = 0 #current read position
      out_data[bi_name] = {}
      
      while rPos <= len(chunk):
        p_name = chunk[rPos:rPos+3].decode('utf-8')
        if p_name == 'END':
          break
        
        # see: https://docs.python.org/3/library/struct.html for types. H=uint16
        type_idx = struct.unpack("<H", chunk[rPos+4:rPos+6])[0]
        p_size = struct.unpack("<H", chunk[rPos+6:rPos+8])[0]
        chunk_sub = chunk[rPos+8:rPos+8+(2*p_size)] #subset of the chunk with just the parameter
        rPos = rPos+8+(2*p_size) #(update for start of next parameter)
        
        p_type = blk_params[type_idx]
        if p_type == 'int':
  #        try:
          fmt_param = struct.unpack("<i", chunk_sub)[0]
  #        except error:
  #          return params
        elif p_type == 'float':
          fmt_param = struct.unpack('<d', chunk_sub)[0]
        elif p_type == 'str':
          p_end = chunk_sub.find(b'\x00')
          fmt_param = chunk_sub[:p_end].decode('latin-1')
        else: #can't tell what it is, just return the bytes
          fmt_param = copy.copy(chunk_sub)
        
        
        out_data[bi_name][p_name] = fmt_param
      
      
  
      
  if 'ScSm' in out_data: #spectra, also return wavenumbers
    out_data['wn'] = np.linspace(out_data['Data Parameters ScSm']['FXV'], out_data['Data Parameters ScSm']['LXV'], 
              out_data['Data Parameters ScSm']['NPT'])
      
  if 'ScSm/2.Chn.' in out_data: #spectra, also return wavenumbers
    out_data['wn/2.Chn.'] = np.linspace(out_data['Data Parameters ScSm/2.Chn.']['FXV'], out_data['Data Parameters ScSm/2.Chn.']['LXV'], 
              out_data['Data Parameters ScSm/2.Chn.']['NPT'])
  
  
  return out_data
  
      
 










    
    
    
    

