#!/usr/bin/env python3

#
# Author: Cunren Liang
# Copyright 2021
#

import os
import glob
import shutil
import datetime
import numpy as np

import isce, isceobj
from isceobj.Alos2Proc.Alos2ProcPublic import create_xml


def datesFromPairs(pairs):
    '''get all dates from pairs
    '''
    dates = []
    for p in pairs:
        for x in p.split('_'):
            if x not in dates:
                dates.append(x)

    dates.sort()

    return dates


def least_sqares(H, S, W=None):
    '''
    #This can make use multiple threads (set environment variable: OMP_NUM_THREADS)
    linear equations:  H theta = s
    W:                 weight matrix
    '''

    S.reshape(H.shape[0], 1)
    if W is None:
        #use np.dot instead since some old python versions don't have matmul
        m1 = np.linalg.inv(np.dot(H.transpose(), H))
        Z = np.dot(       np.dot(m1, H.transpose())           , S)
    else:
        #use np.dot instead since some old python versions don't have matmul
        m1 = np.linalg.inv(np.dot(np.dot(H.transpose(), W), H))
        Z = np.dot(np.dot(np.dot(m1, H.transpose()), W), S)

    return Z.reshape(Z.size)


def interp_2d(data1, numberRangeLooks1, numberRangeLooks2, numberAzimuthLooks1, numberAzimuthLooks2, width2=None, length2=None):
    '''
    interpolate data1 of numberRangeLooks1/numberAzimuthLooks1 to data2 of numberRangeLooks2/numberAzimuthLooks2
    '''
    length1, width1 = data1.shape

    if width2 is None:
        width2 = int(np.around(width1*numberRangeLooks1/numberRangeLooks2))
    if length2 is None:
        length2 = int(np.around(length1*numberAzimuthLooks1/numberAzimuthLooks2))


    #number of range looks input
    nrli = numberRangeLooks1
    #number of range looks output
    nrlo = numberRangeLooks2
    #number of azimuth looks input
    nali = numberAzimuthLooks1
    #number of azimuth looks output
    nalo = numberAzimuthLooks2

    index1 = np.linspace(0, width1-1, num=width1, endpoint=True)
    index2 = np.linspace(0, width2-1, num=width2, endpoint=True) * nrlo/nrli + (nrlo-nrli)/(2.0*nrli)
    data2 = np.zeros((length2, width2), dtype=data1.dtype)
    for i in range(length1):
        f = interp1d(index1, data1[i,:], kind='cubic', fill_value="extrapolate")
        data2[i, :] = f(index2)
    
    index1 = np.linspace(0, length1-1, num=length1, endpoint=True)
    index2 = np.linspace(0, length2-1, num=length2, endpoint=True) * nalo/nali + (nalo-nali)/(2.0*nali)
    for j in range(width2):
        f = interp1d(index1, data2[0:length1, j], kind='cubic', fill_value="extrapolate")
        data2[:, j] = f(index2)

    return data2


def cmdLineParse():
    '''
    command line parser.
    '''
    import sys
    import argparse

    parser = argparse.ArgumentParser(description='least squares estimation')
    parser.add_argument('--idir', dest='idir', type=str, required=True,
            help = 'input directory where each pair (YYYYMMDD_YYYYMMDD) is located. only folders are recognized')
    parser.add_argument('--odir', dest='odir', type=str, required=True,
            help = 'output directory for estimated result of each date')
    parser.add_argument('--zro_date', dest='zro_date', type=str, default=None,
            help = 'date in least squares estimation whose ionospheric phase is assumed to be zero. format: YYYYMMDD. default: first date')

    parser.add_argument('--exc_date', dest='exc_date', type=str, nargs='+', default=[],
            help = 'pairs involving these dates are excluded in least squares estimation. a number of dates seperated by blanks. format: YYYYMMDD YYYYMMDD YYYYMMDD...')
    parser.add_argument('--exc_pair', dest='exc_pair', type=str, nargs='+', default=[],
            help = 'pairs excluded in least squares estimation. a number of pairs seperated by blanks. format: YYYYMMDD-YYYYMMDD YYYYMMDD-YYYYMMDD...')
    parser.add_argument('--tsmax', dest='tsmax', type=float, default=None,
            help = 'maximum time span in years of pairs used in least squares estimation. default: None')

    parser.add_argument('--nrlks1', dest='nrlks1', type=int, default=1,
            help = 'number of range looks of input. default: 1')
    parser.add_argument('--nalks1', dest='nalks1', type=int, default=1,
            help = 'number of azimuth looks of input. default: 1')
    parser.add_argument('--nrlks2', dest='nrlks2', type=int, default=1,
            help = 'number of range looks of output. default: 1')
    parser.add_argument('--nalks2', dest='nalks2', type=int, default=1,
            help = 'number of azimuth looks of output. default: 1')
    parser.add_argument('--width2', dest='width2', type=int, default=None,
            help = 'width of output result. default: None, determined by program')
    parser.add_argument('--length2', dest='length2', type=int, default=None,
            help = 'length of output result. default: None, determined by program')
    parser.add_argument('--merged_geom', dest='merged_geom', type=str, default=None,
            help = 'a merged geometry file for getting width2/length2, e.g. merged/geom_reference/hgt.rdr. if provided, --width2/--length2 will be overwritten')

    parser.add_argument('--interp', dest='interp', action='store_true', default=False,
            help='interpolate estimated result to nrlks2/nalks2 sample size')
    parser.add_argument('--msk_overlap', dest='msk_overlap', action='store_true', default=False,
            help='mask output with overlap of all acquisitions')


    if len(sys.argv) <= 1:
        print('')
        parser.print_help()
        sys.exit(1)
    else:
        return parser.parse_args()


if __name__ == '__main__':

    inps = cmdLineParse()


    #get user parameters from input
    idir = inps.idir
    odir = inps.odir
    dateZero = inps.zro_date
    dateExcluded = inps.exc_date
    pairExcluded = inps.exc_pair
    tsmax = inps.tsmax
    numberRangeLooks1 = inps.nrlks1
    numberAzimuthLooks1 = inps.nalks1
    numberRangeLooks2 = inps.nrlks2
    numberAzimuthLooks2 = inps.nalks2
    width2 = inps.width2
    length2 = inps.length2
    mergedGeom = inps.merged_geom
    interp = inps.interp
    maskOverlap = inps.msk_overlap
    #######################################################

    #all pair folders in order
    pairDirs = sorted(glob.glob(os.path.join(os.path.abspath(idir), '*_*')))
    pairDirs = [x for x in pairDirs if os.path.isdir(x)]

    #all pairs in order
    pairsAll = [os.path.basename(x) for x in pairDirs]
    #all dates in order
    datesAll = datesFromPairs(pairsAll)

    #select pairs
    pairs = []
    for x in pairsAll:
        dateReference, dateSecondary = x.split('_')
        timeReference = datetime.datetime.strptime(dateReference, "%Y%m%d")
        timeSecondary = datetime.datetime.strptime(dateSecondary, "%Y%m%d")
        ts = np.absolute((timeSecondary - timeReference).total_seconds()) / (365.0 * 24.0 * 3600)
        if (dateReference in dateExcluded) and (dateSecondary in dateExcluded):
            continue
        if (x in pairExcluded):
            continue
        if tsmax is not None:
            if ts > tsmax:
                continue
        pairs.append(x)

    dates = datesFromPairs(pairs)
    if dateZero is not None:
        if dateZero not in dates:
            raise Exception('zro_date provided by user not in the dates involved in least squares estimation.')
    else:
        dateZero = dates[0]

    print(f'all pairs ({len(pairsAll)}):\n{pairsAll}')
    print(f'all dates ({len(datesAll)}):\n{datesAll}')
    print(f'used pairs ({len(pairs)}):\n{pairs}')
    print(f'used dates ({len(dates)}):\n{dates}')


####################################################################################
    print('\nSTEP 1. read files')
####################################################################################

    ndate = len(dates)
    npair = len(pairs)

    ionfile = os.path.join(idir, pairs[0], 'ion_cal', 'filt.ion')

    img = isceobj.createImage()
    img.load(ionfile+'.xml')
    width = img.width
    length = img.length

    ionPairs = np.zeros((npair, length, width), dtype=np.float32)
    flag = np.ones((length, width), dtype=np.float32)

    #this is reserved for use
    wls = False
    stdPairs = np.ones((npair, length, width), dtype=np.float32)
    for i in range(npair):
        ionfile = os.path.join(idir, pairs[i], 'ion_cal', 'filt.ion')
        ionPairs[i, :, :] = (np.fromfile(ionfile, dtype=np.float32).reshape(length*2, width))[1:length*2:2, :]
        #flag of valid/invalid is defined by amplitde image
        amp = (np.fromfile(ionfile, dtype=np.float32).reshape(length*2, width))[0:length*2:2, :]
        flag *= (amp!=0)


####################################################################################
    print('\nSTEP 2. do least squares')
####################################################################################
    import copy
    from numpy.linalg import matrix_rank
    dates2 = copy.deepcopy(dates)
    dates2.remove(dateZero)

    #observation matrix
    H0 = np.zeros((npair, ndate-1))
    for k in range(npair):
        dateReference = pairs[k].split('_')[0]
        dateSecondary = pairs[k].split('_')[1]
        if dateReference != dateZero:
            dateReference_i = dates2.index(dateReference)
            H0[k, dateReference_i] = 1
        if dateSecondary != dateZero:
            dateSecondary_i = dates2.index(dateSecondary)
            H0[k, dateSecondary_i] = -1
    rank = matrix_rank(H0)
    if rank < ndate-1:
        raise Exception('dates to be estimated are not fully connected by the pairs used in least squares')
    else:
        print('number of pairs to be used in least squares: {}'.format(npair))
        print('number of dates to be estimated: {}'.format(ndate-1))
        print('observation matrix rank: {}'.format(rank))

    ts = np.zeros((ndate-1, length, width), dtype=np.float32)
    for i in range(length):
        if (i+1) % 50 == 0 or (i+1) == length:
            print('processing line: %6d of %6d' % (i+1, length), end='\r')
        if (i+1) == length:
            print()
        for j in range(width):

            #observed signal
            S0 = ionPairs[:, i, j]

            if wls == False:
                #observed signal
                S = S0
                H = H0
            else:
                #add weight
                #https://stackoverflow.com/questions/19624997/understanding-scipys-least-square-function-with-irls
                #https://stackoverflow.com/questions/27128688/how-to-use-least-squares-with-weight-matrix-in-python
                wgt = (stdPairs[:, i, j])**2
                W = np.sqrt(1.0/wgt)
                H = H0 * W[:, None]
                S = S0 * W

            #do least-squares estimation
            #[theta, residuals, rank, singular] = np.linalg.lstsq(H, S)
            #make W full matrix if use W here (which is a slower method)
            #'using W before this' is faster
            theta = least_sqares(H, S, W=None)
            ts[:, i, j] = theta

    # #dump raw estimate
    # cdir = os.getcwd()
    # os.makedirs(odir, exist_ok=True)
    # os.chdir(odir)

    # for i in range(ndate-1):
    #     file_name = 'filt_ion_'+dates2[i]+ml2+'.ion'
    #     ts[i, :, :].astype(np.float32).tofile(file_name)
    #     create_xml(file_name, width, length, 'float')
    # file_name = 'filt_ion_'+dateZero+ml2+'.ion'
    # (np.zeros((length, width), dtype=np.float32)).astype(np.float32).tofile(file_name)
    # create_xml(file_name, width, length, 'float')

    # os.chdir(cdir)


####################################################################################
    print('\nSTEP 3. interpolate ionospheric phase')
####################################################################################
    from scipy.interpolate import interp1d

    width1 = width
    length1 = length

    if width2 is None:
        width2 = int(width1 * numberRangeLooks1 / numberRangeLooks2)
    if length2 is None:
        length2 = int(length1 * numberAzimuthLooks1 / numberAzimuthLooks2)
    if mergedGeom is not None:
        from osgeo import gdal
        ds = gdal.Open(mergedGeom + ".vrt", gdal.GA_ReadOnly)
        width2 = ds.RasterXSize
        length2 = ds.RasterYSize

    os.makedirs(odir, exist_ok=True)
    for idate in range(ndate-1):
        print('interplate {}'.format(dates2[idate]))

        ionrectfile = os.path.join(odir, dates2[idate]+'.ion')
        if interp and ((numberRangeLooks1 != numberRangeLooks2) or (numberAzimuthLooks1 != numberAzimuthLooks2)):
            ionrect = interp_2d(ts[idate, :, :], numberRangeLooks1, numberRangeLooks2, numberAzimuthLooks1, numberAzimuthLooks2, 
                                width2=width2, length2=length2)

            #mask with overlap of all acquistions
            if maskOverlap:
                if idate == 0:
                    flagrect = interp_2d(flag, numberRangeLooks1, numberRangeLooks2, numberAzimuthLooks1, numberAzimuthLooks2, 
                                    width2=width2, length2=length2)
                ionrect *= (flagrect>0.5)

            ionrect.astype(np.float32).tofile(ionrectfile)
            create_xml(ionrectfile, width2, length2, 'float')
        else:
            ionrect = ts[idate, :, :]

            if maskOverlap:
                ionrect *= flag

            ionrect.astype(np.float32).tofile(ionrectfile)
            create_xml(ionrectfile, width1, length1, 'float')

    ionrectfile = os.path.join(odir, dateZero+'.ion')
    if interp and ((numberRangeLooks1 != numberRangeLooks2) or (numberAzimuthLooks1 != numberAzimuthLooks2)):
        (np.zeros((length2, width2), dtype=np.float32)).astype(np.float32).tofile(ionrectfile)
        create_xml(ionrectfile, width2, length2, 'float')
    else:
        (np.zeros((length1, width1), dtype=np.float32)).astype(np.float32).tofile(ionrectfile)
        create_xml(ionrectfile, width1, length1, 'float')




