#!/usr/bin/env python3

# QDC Converter
# Special credits to an author of an original VBA script.

import csv
import os
import struct
from types import SimpleNamespace

import click
from click_option_group import optgroup
import numpy as np
from tqdm import tqdm

LAYER_PARAMETERS = {
    0: {
        'a_step': 90 / 2 ** 22,
        'n_sectors': 7,
        'f_size1': 372736,
        'f_offset1': 4097,
        'f_size2': 352256,
        'f_offset2': 4097,
        'f_size3': -1,
        'f_offset3': 0,
        'f_size4': -1,
        'f_offset4': 0,
        'l_size': 256,
        'l_size2': 256,
    },
    1: {
        'a_step': 90 / 2 ** 21,
        'n_sectors': 3,
        'f_size1': 372736,
        'f_offset1': 266241,
        'f_size2': 352256,
        'f_offset2': 266241,
        'f_size3': 110592,
        'f_offset3': 4097,
        'f_size4': 90112,
        'f_offset4': 4097,
        'l_size': 128,
        'l_size2': 128,
    },
    2: {
        'a_step': 90 / 2 ** 20,
        'n_sectors': 1,
        'f_size1': 372736,
        'f_offset1': 331777,
        'f_size2': 352256,
        'f_offset2': 331777,
        'f_size3': 110592,
        'f_offset3': 69633,
        'f_size4': 90112,
        'f_offset4': 69633,
        'l_size': 64,
        'l_size2': 64,
    },
    3: {
        'a_step': 90 / 2 ** 19,
        'n_sectors': 0,
        'f_size1': 372736,
        'f_offset1': 348161,
        'f_size2': 352256,
        'f_offset2': 348161,
        'f_size3': 110592,
        'f_offset3': 86017,
        'f_size4': 90112,
        'f_offset4': 86017,
        'l_size': 32,
        'l_size2': 32,
    },
    4: {
        'a_step': 90 / 2 ** 18,
        'n_sectors': 1,
        'f_size1': 372736,
        'f_offset1': 352257,
        'f_size2': -1,
        'f_offset2': 0,
        'f_size3': 110592,
        'f_offset3': 90113,
        'f_size4': -1,
        'f_offset4': 0,
        'l_size': 64,
        'l_size2': 16,
    },
    5: {
        'a_step': 90 / 2 ** 17,
        'n_sectors': 0,
        'f_size1': 372736,
        'f_offset1': 368641,
        'f_size2': -1,
        'f_offset2': 0,
        'f_size3': 110592,
        'f_offset3': 106497,
        'f_size4': -1,
        'f_offset4': 0,
        'l_size': 32,
        'l_size2': 8,
    },
}

def get_files_recursively(dir_path, exts):
    '''
    Рекурсивный поиск файлов с расширениеми указанными в :param exts:.
    '''
    if type(exts) not in (list, tuple):
        exts = (exts,)
    result = []
    files = [os.path.join(dp, f) for dp, dn, fn in os.walk(dir_path) for f in fn]
    for f in files:
        if any(f.endswith(ext) for ext in exts):
            full_path_f = os.path.join(dir_path, f)
            result.append(full_path_f)
    return result

@click.command()
@optgroup.group('Основные параметры', help='Ключевые параметры конвертера')
@optgroup.option('--qdc-folder-path', '-i', required=True, type=click.Path(exists=True, resolve_path=True, file_okay=False, dir_okay=True), help='Путь до папки со вложенными контурами QuickDraw Contours (QDC).')
@optgroup.option('--output-path', '-o', required=True, type=click.Path(exists=False, resolve_path=True, file_okay=True, dir_okay=False), help='Путь до сконвертированного файла (*.csv или *.grd).')
@optgroup.option('--layer', '-l', required=True, type=click.IntRange(0, 5), metavar='[0,1,2,3,4,5]', help='Слой данных (0 - Raw user data, 1 - Recommended).')
@optgroup.option('--validity-codes', '-vc', is_flag=True, help='Записывать код качества вместо глубины.')
@optgroup.option('--quite', '-q', is_flag=True, help='"Молчаливый режим"')
@optgroup.group('Параметры корректировки', help='Корректировки')
@optgroup.option('--x-correction', '-dx', type=click.FLOAT, default=0.0, help='Корректировка X.')
@optgroup.option('--y-correction', '-dy', type=click.FLOAT, default=0.0, help='Корректировка Y.')
@optgroup.option('--z-correction', '-dz', type=click.FLOAT, default=0.0, help='Корректировка Z.')
@optgroup.group('CSV Параметры', help='Параметры касающиеся записи CSV таблицы')
@optgroup.option('--csv-delimiter', '-csvd', type=click.STRING, default=',', help='CSV разделитель значений колонок.')
@optgroup.option('--csv-skip-headers', '-csvs', is_flag=True, help='Не записывать заголовок таблицы.')
@optgroup.option('--csv-yxz', '-csvy', is_flag=True, help='Изменить порядок записи с X,Y,Z на Y,X,Z в CSV таблице.')
def convert(qdc_folder_path, output_path, layer, validity_codes, quite, x_correction, y_correction, z_correction, csv_delimiter, csv_skip_headers, csv_yxz):
    '''
    QDC конвертер.
    '''
    output_path_ext = os.path.splitext(output_path)[-1]
    if output_path_ext.lower() not in ('.csv', '.grd'):
        raise click.UsageError('Расширение выходного файла должны быть *.csv (CSV таблица) или *.grd (ESRI ASCII grid)')
    
    layer_parameters = SimpleNamespace(**LAYER_PARAMETERS[layer])
    qdc_files = get_files_recursively(qdc_folder_path, 'qdc')
    
    # calculate boundaries
    x_min, y_min = 32000, 32000
    x_max, y_max = -32000, -32000
    for qdc_file in qdc_files:
        qdc_file_size = os.path.getsize(qdc_file)
        if qdc_file_size in (layer_parameters.f_size1, layer_parameters.f_size2, \
                layer_parameters.f_size3, layer_parameters.f_size4):
            with open(qdc_file, 'rb') as f_qdc:
                f_qdc.seek(164)
                val = struct.unpack('<h', f_qdc.read(2))[0]
                x_min = min(val, x_min)
                x_max = max(val, x_max)
                
                f_qdc.seek(160)
                val = struct.unpack('<h', f_qdc.read(2))[0]
                y_min = min(val, y_min)
                y_max = max(val, y_max)

    if x_min == 32000 or y_min == 32000 \
        or x_max == -32000 or y_max == -32000:
        raise ValueError('Не найдено валидных QDC файлов!')

    x_size = (x_max - x_min + 1) * layer_parameters.l_size - 1
    y_size = (y_max - y_min + 1) * layer_parameters.l_size - 1
    arr_depth = np.zeros((x_size + 1, y_size + 1), dtype=np.int16)

    # calculate depth array
    for qdc_file in tqdm(qdc_files, desc='Подсчет карты глубины', disable=quite):
        qdc_file_size = os.path.getsize(qdc_file)
        if qdc_file_size in (layer_parameters.f_size1, layer_parameters.f_size2, \
                layer_parameters.f_size3, layer_parameters.f_size4):
            with open(qdc_file, 'rb') as f_qdc:
                f_qdc.seek(164)
                val = struct.unpack('<h', f_qdc.read(2))[0]
                x_orig = (val - x_min) * layer_parameters.l_size2
                
                f_qdc.seek(160)
                val = struct.unpack('<h', f_qdc.read(2))[0]
                y_orig = (val - y_min) * layer_parameters.l_size2

                if qdc_file_size == layer_parameters.f_size1:
                    i = layer_parameters.f_offset1
                elif qdc_file_size == layer_parameters.f_size2:
                    i = layer_parameters.f_offset2
                elif qdc_file_size == layer_parameters.f_size3:
                    i = layer_parameters.f_offset3
                elif qdc_file_size == layer_parameters.f_size4:
                    i = layer_parameters.f_offset4

                for yy in range(layer_parameters.n_sectors + 1):
                    for xx in range(layer_parameters.n_sectors + 1):
                        for y in range(32):
                            for x in range(32):
                                x_abs = xx * 32 + x + x_orig
                                y_abs = yy * 32 + y + y_orig
                                f_qdc.seek(i + 1)
                                val_code = struct.unpack('<h', f_qdc.read(2))[0]  # read validity code
                                
                                if validity_codes:  # write validity codes to array instead of depth
                                    arr_depth[x_abs, y_abs] = val_code
                                else:
                                    if val_code != 0:
                                        f_qdc.seek(i - 1)
                                        val_depth = struct.unpack('<h', f_qdc.read(2))[0]  # read depth in cm
                                        arr_depth[x_abs, y_abs] = val_depth

                                i += 4

    x_orig = x_min * 90 / 2 ** 14
    y_orig = y_min * 90 / 2 ** 14

    f_fix = lambda x: np.sign(x) * np.int16(np.abs(x))

    # save depth array to *.csv or *.grd
    if output_path_ext.lower() == '.grd':
        # ESRI ASCII grid
        with open(output_path, 'w') as f_grd:
            f_grd.write(f'NCOLS {x_size + 1}\n')
            f_grd.write(f'NROWS {y_size + 1}\n')
            f_grd.write(f'XLLCORNER {x_orig}\n')
            f_grd.write(f'YLLCORNER {y_orig}\n')
            f_grd.write(f'CELLSIZE {layer_parameters.a_step}\n')
            f_grd.write(f'NODATA_VALUE 0\n')

            for j in tqdm(range(y_size, -1, -1), desc='Сохранение растра Esri ASCII'):
                row_values = []
                for i in range(x_size + 1):
                    if validity_codes:
                        t_val = f_fix(arr_depth[i, j] / 4096)
                        z = t_val * 10 + (arr_depth[i, j] - t_val * 4096) / 256
                    else:
                        z = arr_depth[i, j] / 100
                    row_values.append(z + z_correction)
                f_grd.write(' '.join(str(x) for x in row_values) + '\n')

        # write projection file
        output_path_prj = output_path[:-4] + '.prj'
        with open(output_path_prj, 'w') as f_prj:
            f_prj.write('GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]')
    
    elif output_path_ext.lower() == '.csv':
        # CSV table
        with open(output_path, 'w', newline='') as f_csv:
            writer = csv.writer(f_csv, delimiter=csv_delimiter)

            # write header
            if not csv_skip_headers:
                if csv_yxz:
                    if validity_codes:
                        writer.writerow(['Y', 'X', 'ValCode'])
                    else:
                        writer.writerow(['Y', 'X', 'Depth(m)'])
                else:
                    if validity_codes:
                        writer.writerow(['X', 'Y', 'ValCode'])
                    else:
                        writer.writerow(['X', 'Y', 'Depth(m)'])

            # write data
            for j in tqdm(range(y_size, -1, -1), desc='Сохранение CSV таблицы', disable=quite):
                for i in range(x_size + 1):
                    if arr_depth[i, j] > 0:  # skip all 0 values
                        if validity_codes:
                            t_val = f_fix(arr_depth[i, j] / 4096)
                            z = t_val * 10 + (arr_depth[i, j] - t_val * 4096) / 256
                        else:
                            z = arr_depth[i, j] / 100
                         
                        x = x_orig + layer_parameters.a_step / 2 + i * layer_parameters.a_step  # adding a_step / 2 to move point to the middle of the cell extent
                        y = y_orig + layer_parameters.a_step / 2 + j * layer_parameters.a_step  # adding a_step / 2 to move point to the middle of the cell extent
                        
                        if csv_yxz:
                            writer.writerow([y + y_correction, x + x_correction, z + z_correction])
                        else:
                            writer.writerow([x + x_correction, y + y_correction, z + z_correction])

if __name__ == '__main__':
    convert()
