#!/usr/bin/env python3
# Part of TotalDepth: Petrophysical data processing and presentation.
# Copyright (C) 2011-2021 Paul Ross
#
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Paul Ross: apaulross@gmail.com
"""
Created on 10 Nov 2010

@author: p2ross
"""
import os

__author__  = 'Paul Ross'
__date__    = '2010-08-02'
__version__ = '0.1.0'
__rights__  = 'Copyright (c) Paul Ross'

import logging
import struct
import sys
import time
import typing

from TotalDepth.LIS import lis_cmn_cmd_opts
from TotalDepth.LIS.core import PhysRec, File
# from TotalDepth.LIS.core.File import scan_file_no_output, scan_file_with_different_padding, find_best_padding_options
from TotalDepth.common import cmn_cmd_opts
from TotalDepth.util import Histogram, DirWalk

# One byte for type, on for attributes
LD_STRUCT_HEAD = struct.Struct('>BB')
assert(LD_STRUCT_HEAD.size == 2)


def scan_file(fp, keepGoing, pad_modulo, pad_non_null, theS=sys.stdout):
    try:
        myPrh = PhysRec.PhysRecRead(fp, fp, keepGoing, pad_modulo, pad_non_null=pad_non_null)
    except PhysRec.ExceptionPhysRec as err:
        print('Can not open file, error: %s' % str(err))
        return
    # Now other stuff generated by this loop
    myHeader = myPrh.strHeader(inc_attributes_short=True) + '   LR Attr [Total LD]'
    theS.write(myHeader)
    theS.write('\n')
    theS.write(' start '.center(len(myHeader), '-'))
    myLdSum = -1
    numPR = 0
    # Histogram of PR lengths
    myLenHist = Histogram.Histogram()
    for myLd, isLdStart in myPrh.genLd():
        myLenHist.add(myPrh.prLen)
        if isLdStart:
            if myLdSum == -1:
                # First time through the loop then don't write anything
                pass
            else:
                # This is not the first time through the loop
                # so write out the trailing LogicalData length
                theS.write(' [%8d]' % myLdSum) 
            myLdSum = 0
        myLdSum += len(myLd)
        theS.write('\n')
        theS.write(str(myPrh))
        theS.write('  ')
        theS.write(myPrh.attribute_str_short())
        if isLdStart:
            if len(myLd) >= 2:
                h, a = LD_STRUCT_HEAD.unpack(myLd[:LD_STRUCT_HEAD.size])
                theS.write(' 0x{0:02X} 0x{1:02x}'.format(h, a))
            else:
                theS.write(' 0x??')
        else:
            theS.write(' + --   --')
        numPR += 1
    theS.write(' [%8d]' % myLdSum)
    theS.write('\n')
    theS.write('%s\n' % str(myPrh))
    theS.write(' EOF '.center(len(myHeader), '-'))
    theS.write('\n')
    theS.write('PR Count: %d\n' % numPR)
    theS.write('Histogram of Physical Record lengths:\n')
    theS.write(myLenHist.strRep(100, valTitle='Bytes', inclCount=True))
    theS.write('\n')


class PhysRecScanResult(typing.NamedTuple):
    path: str
    pr_count: int
    error: bool

    def __str__(self):
        return f'{self.error!r:5} {self.pr_count:8,d} {self.path}'


def scan_directory(
        file_path: str,
        recursive: bool,
        keep_going: bool,
        pad_modulo: int,
        pad_non_null: bool) -> typing.Dict[str, PhysRecScanResult]:
    ret = {}
    for file_in_out in DirWalk.dirWalk(file_path, '', '', recursive=recursive, bigFirst=False):
        try:
            pr_count = File.scan_file_no_output(file_in_out.filePathIn, keep_going, pad_modulo, pad_non_null)
            error = False
        except PhysRec.ExceptionPhysRec as err:
            logging.exception(f'Can not read {file_in_out.filePathIn}')
            pr_count = 0
            error = True
        ret[file_in_out.filePathIn] = PhysRecScanResult(file_in_out.filePathIn, pr_count, error)
    return ret


class PhysRecScanResultWithPad(typing.NamedTuple):
    result: PhysRecScanResult
    pad_modulo: int
    pad_non_null: bool

    def __str__(self):
        return f'{self.pad_modulo:2d} {self.pad_non_null!r:5} {self.result!s}'


def scan_directory_with_different_padding(
        file_path: str,
        recursive: bool,
        keep_going: bool) -> typing.Dict[str, PhysRecScanResultWithPad]:
    ret = {}
    for file_in_out in DirWalk.dirWalk(file_path, '', '', recursive=recursive, bigFirst=False):
        result = File.best_physical_record_pad_settings(file_in_out.filePathIn, keep_going)
        if result is not None:
            pr_count = File.scan_file_no_output(file_in_out.filePathIn, keep_going, result[0], result[1])
            error = False
        else:
            pr_count = 0
            error = True
        if result is not None:
            ret[file_in_out.filePathIn] = PhysRecScanResultWithPad(
                PhysRecScanResult(file_in_out.filePathIn, pr_count, error),
                *result)
        else:
            ret[file_in_out.filePathIn] = PhysRecScanResultWithPad(
                PhysRecScanResult(file_in_out.filePathIn, pr_count, error),
                -1, False)
    return ret


def main():
    usage = """usage: %(prog)s [options] file
Scans a LIS79 file and reports Physical Record structure."""
    print('Cmd: %s' % ' '.join(sys.argv))
    arg_parser = cmn_cmd_opts.path_in(usage, prog='TotalDepth.LIS.ScanPhysRec', version='%(prog)s ' + __version__)
    lis_cmn_cmd_opts.add_physical_record_padding_options(arg_parser)
    arg_parser.add_argument(
        "--pad-opts-all", action="store_true", default=False,
        help=(
            "Explores all the padding options and reports which works."
            " This ignores --pad-modulo and --pad-non-null."
            " For file argument only, not directory."
            " Default: %(default)s."
        )
    )

    cmn_cmd_opts.add_log_level(arg_parser, level=20)
    args = arg_parser.parse_args()
    print(args)
    cmn_cmd_opts.set_log_level(args)
    clk_start = time.perf_counter()
    # Your code here
    if os.path.isfile(args.path_in):
        if args.pad_opts_all:
            result = File.scan_file_with_different_padding(args.path_in, args.keepGoing)
            for k in sorted(result.keys()):
                print(f'Pad options: {k!s:12} PRs {result[k]:8,d}')
            best_pad_options = File.ret_padding_options_with_max_records(result)
            print(f'Best pad options: {best_pad_options}')
        else:
            if args.verbose:
                scan_file(args.path_in, args.keepGoing, args.pad_modulo, args.pad_non_null)
            else:
                pr_count = File.scan_file_no_output(args.path_in, args.keepGoing, args.pad_modulo, args.pad_non_null)
                print(f'Scan found {pr_count} Physical Records. Use -v to see the details.')
    elif os.path.isdir(args.path_in):
        fails = 0
        if args.pad_opts_all:
            results = scan_directory_with_different_padding(args.path_in, args.recurse, args.keepGoing)
            for r in sorted(results.keys()):
                print(results[r])
        else:
            results = scan_directory(args.path_in, args.recurse, args.keepGoing, args.pad_modulo, args.pad_non_null)
            for r in sorted(results.keys()):
                print(results[r])
                if results[r].error:
                    fails += 1
        print(f'Total files: {len(results):8,d}')
        print(f'    Success: {len(results) - fails:8,d}')
        print(f'    Failure: {fails:8,d}')
    else:
        logging.error(f'{args.path_in} does not exist.')
    clk_exec = time.perf_counter() - clk_start
    print('CPU time = %8.3f (S)' % clk_exec)
    print('Bye, bye!')
    return 0


if __name__ == '__main__':
    sys.exit(main())
