#!/usr/bin/env python
"""
.. include:: ../docs/stack_files.md
"""
from pyaddons import __version__, __name__ as PKG_NAME
from simple_progress import progress
from pyaddons import file_helper, gzopen
import argparse
import sys
import csv
import os
import re
import hashlib

csv.field_size_limit(sys.maxsize)


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class FileStack(object):
    """
    Merge (stack) a bunch of files and yield the result lines.  This acts like
    unix `cat` but is slower and also performs more checks. The files should be
    tabular with a header. if no delimiter is specified then it is determined
    with `csv.Sniffer`. If all the files do have the same delimiter then a
    fixed delimiter can be supplied. Under default conditions it is assumed
    that the headers are identical, however, there is an option to group
    certain column names into a proxy column that will contain their data in
    the output.
    """

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, infiles, columns=None, dialect=None, delete=False,
                 unsafe=False, md5row=False, regexp_id=None, sniff_bytes=8192):
        """
        Initialise with:

        Parameters
        ----------
        infiles :list of str
            Inputfiles that need to be merged
        columns : NoneType or list of str or dict, optional
            A subset of columns that are required to be output, rather than
            all of the columns. If None, then all columns are output and the
            headers of each file must be the same. If defined, then the header
            can appear in any order. If the list element is a str, then it is
            assumed that the column name must appear in all files. If it is a
            tuple, then the first element is the output column name in the
            header and the second element should be another tuple of possible
            column names that will appear in each file. It is assumed that only
            one of the possible column names should appear in each file and it
            is an error if > than one appears (default=`NoneType`)
        dialect : :obj:`csv.Dialect` or NoneType, optional
            If defined it is assumed that the dialect will be the same in all
            files. However, if `NoneType` then `csv.Sniffer` is used to attempt
            to detect the dialect of each file (default=`NoneType`)
        delete : bool, optional
            Should the files to be merged be deleted after merging
            (default=`False`)
        unsafe : bool, optional
            Should the merged files be deleted after each file is processed
            rather than after a full merge. This is potentially unsafe as the
            merge may fail. Even if this is True, all the file headers are
            checked before the merge to account for the possibility that there
            are header errors, also MD5sums are taken so this ensures that the
            files can be read to completion. This option can be useful if disk
            space is limited and the files are big. Only used if delete is
            True. (default=`False`)
        md5rom : bool, optional
            Should an MD5 be taken of each row that is output, this is
            sometimes useful for uniqueing on rows (default=`False`)
        regexp_id : :obj:`re.Pattern` or NoneType, optional
            This generates an ID column based on a REGEXP applied to the input
            file name, the regexps must create unique IDs within all the files
            this is done prior to merge. (default=`NoneType`)
        sniff_bytes : int
            If no delimiter is specified, then this is the number of bytes that
            is read in from the file when attemping to detect the delimiter

        Raises
        ------
        ValueError
            If there are no input files supplied
        """
        self._infiles = infiles
        self._columns = columns
        self._dialect = dialect
        self._delete = delete
        self._unsafe = unsafe
        self._md5row = md5row
        self._regexp_id = regexp_id
        self._sniff_bytes = sniff_bytes

        # We use these flags to keep track of the state of the object
        # Have the files been initialised (checked)
        self._is_initialised = False

        # Have all the files already been merged
        self._is_merged = False

        # Is the object being used via the context manager
        self._using_context_manager = False

        self._log = []

        # Make sure that we actually have some input files
        if len(self._infiles) == 0:
            raise ValueError("there are no input files")

        # Additional header is the columns that will be added to the existing
        # header. i.e. the additional columns potentially added by the stacker
        self._additional_header = []

        # The function calls to add the data to a row. We initialise to a dummy
        # function call and then when we have that add data option switched on
        # we will swap out the function call and add the column to the header.
        # This relies on a similar interface between functions
        self._md5row_func = self._dummy
        self._regexp_id_func = self._dummy
        if md5row is True:
            self._additional_header.append('md5row')
            self._md5row_func = self._add_row_md5

        if regexp_id is not None:
            self._additional_header.append('regexp_id')
            self._regexp_id_func = self._add_regexp_id

        self._additional_header.append('md5file')

        # These indexes will be used in the iterator to select the current file
        # to work on
        self._curr_file_idx = -1
        self.curr_file_name = ""
        self.curr_file_ncols = 0
        self.curr_file_info = None
        self.curr_file_reader = None
        self.curr_file_handle = None

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __enter__(self):
        """
        Entry point for the context manager
        """
        self._using_context_manager = True

        # Start initialisation
        self.initialise()
        return self

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __exit__(self, exc_type, exc_value, exc_traceback):
        """
        The exit point for the context manager. If we have a clean exit then
        the files will be deleted if delete is `True`

        Parameters
        ----------
        exc_type
        exc_value
        exc_traceback
        """
        self.close()

        if exc_type is None:  # clean exit
            # If we want to delete all the files upon a clean exit
            if self._delete is True and self._unsafe is False:
                for i in self._log:
                    # print("delete: {0}".format(i['filename']))
                    os.unlink(i['filename'])
        else:
            print("error exit")

        self._using_context_manager = False

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __next__(self):
        """
        Return the current line in the current file

        Returns
        -------
        row : list of str
            The current row from the current file

        Raises
        ------
        StopIteration
            If we have finished all the rows in all the files
        """
        try:
            return self._get_row()
        except StopIteration:
            # The stop iteration is from reaching the end of the file. So when
            # we do we setup the next file and get a row. However, set_next_file
            # can also induce a stop iteration if we run out of files, in
            # which case that will be the end of the execution and we will
            # move onto finalising
            self.set_next_file()
            return self._get_row()

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __iter__(self):
        """
        The entry point for the iterator
        """
        return self

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @property
    def nrows(self):
        """
        Return the total number of rows for the merge files. If there is no
        initialisation then nrows will be -1

        Returns
        -------
        total_rows : int
            The number of rows in all the files (including the header lines).
            If this is not known due to no initialisation or unsafe is not used
            then total_rows will be -1
        """
        if len(self._log) == 0:
            return -1

        try:
            return sum([i['nrows'] for i in self._log])
        except TypeError:
            return -1

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @property
    def header(self):
        """
        Return the header for the merged file
        """
        # If we are not initialised then the header is empty
        if self._is_initialised is False:
            return []

        # Otherwise, if we have defined columns, then the header is either the
        # column name or the name of the proxy column for a column group
        if self._columns is not None:
            header = []

            for c in self._columns:
                if isinstance(c, (tuple, list)):
                    header.append(c[0])
                else:
                    header.append(c)

            return header + self._additional_header

        # Finally, no columns have been supplied so the header will be the same
        # for every file being stacked, so return a copy of the header of the
        # first file
        return self._log[0]['header_row'].copy() + self._additional_header

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _get_row(self):
        """

        """
        # Get the row, now add the bits onto it
        row = next(self.curr_file_reader)
        assert self.curr_file_ncols == len(row), \
            "bad row length '{0}': '{1}'".format(len(row), self.curr_file_name)
        self._md5row_func(row)
        self._regexp_id_func(row, self.curr_file_info)
        row.append(self.curr_file_info['md5hash'])
        return row

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def initialise(self):
        """
        Initialise before the merge, this checks all the headers and
        requested columns (if any) and also generates an MD5 sum of each
        file
        """
        # Make sure any logs are cleared and we set the initialised status
        # to false (there is a possibility that we can initialise more than
        # once if we are not using the context manager)
        self._log = []
        self._is_initialised = False
        self._curr_file_idx = -1
        self.curr_file_name = ""
        self.curr_file_ncols = 0
        self.curr_file_info = None
        self.curr_file_reader = None
        self.curr_file_obj = None

        finfo_args = {'columns': self._columns, 'dialect': self._dialect,
                      'ncols': False, 'md5hash': True, 'errors': True,
                      'regexp_id': self._regexp_id,
                      'sniff_bytes': self._sniff_bytes}

        if self._unsafe is True:
            finfo_args['ncols'] = True

        finfo = file_helper.FileInfo(self._infiles, **finfo_args)
        self._log = [info for info in finfo]

        # Make sure the first file is racked up and ready to go
        self.set_next_file()
        self._is_initialised = True

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def open_file(self, file_info):
        """
        Open the file so it can be output

        Parameters
        ----------
        file_info : :obj:`OrderedDict`
            A file info `OrderedDict` as returned by `file_helper.FileInfo`.
            Specifically the `OrderedDict` should have a `filename` key, and
            a dialect Key
        """

        fobj = gzopen.gzip_fh(file_info['filename'])
        reader = csv.reader(fobj, dialect=file_info['dialect'])

        return fobj, reader

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def set_next_file(self):
        """
        Close the previous file and setup the next file for processing
        """
        # If we are deleting as we go!
        if self._delete is True and self._unsafe is True:
            self.delete()

        self.close()
        self._curr_file_idx += 1

        try:
            self.curr_file_info = self._log[self._curr_file_idx]
        except IndexError as e:
            self._curr_file_idx = -1
            raise StopIteration("no more files") from e

        self.curr_file_name = self.curr_file_info['filename']
        self.curr_file_ncols = self.curr_file_info['header_len']

        self.curr_file_obj, self.curr_file_reader = \
            self.open_file(self.curr_file_info)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def close(self):
        """
        Close any open files
        """
        if self.curr_file_obj is not None:
            self.curr_file_obj.close()
            self.curr_file_reader = None
            self.curr_file_obj = None

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def delete(self):
        """
        Delete the file
        """
        # Make sure we have closed
        self.close()
        os.unlink(self.curr_file_name)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _dummy(self, row, *args):
        """
        A dummy function call that does not do anything but is replcated by
        functions that do do something when certain options are switched on

        Parameters
        ----------
        row : list of str
            A row that will just be passed back to the caller

        Returns
        -------
        row : list of str
            The exact same row that was passed
        """
        return row

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _add_regexp_id(self, row, info):
        """
        Add a regexp ID to the rows
        """
        row.append(info['regexp_id'])
        return row

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _add_row_md5(self, row):
        """
        Add a regexp ID to the rows
        """
        row.append(hashlib.md5('|'.join(row).encode('utf-8')).hexdigest())
        return row


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def init_cmd_line_args(description=None):
    """
    Use argparse to initialise the command line arguments. Note that this does
    not actually parse the arguments but returns the parser objects so that
    other arguments can be added if needed.

    Parameters
    ----------
    description : str or NoneType, optional
        The description for the `--help` of the argument parse. If this is
        `NoneType` then the deault description is used.

    Returns
    -------
    args : :obj:`argparse.ArgumentParser`
        The parser with the default commandline arguments set
    """

    if description is None:
        description = ('A helper script to merge a bunch of files to form a'
                       'single file. This acts like `cat` but is slower and '
                       'also performs more checks. So the files should be '
                       'tabular with. The delimiters can be detected and the '
                       'headers must be the same unless `--columns` is passed. '
                       'However, there is also an option to supply groups of '
                       'column names that will act as a proxy for a single'
                       ' column. It will also produce a merge log file '
                       'and there are options to leave the files being merged'
                       ' intact or to have them removed during merging, either'
                       ' as the merge is being done (--unsafe) or after the'
                       ' merge has been completed')

    parser = argparse.ArgumentParser(
        description=description)

    parser.add_argument('infiles', type=str, nargs='*',
                        help="The input files to extract from, It is expected"
                        "that all input files have a header. The files can be"
                        "compressed (gzip) or uncompressed (default=None)")
    parser.add_argument('-i', '--indir', type=str,
                        help="The input directory, this should be set if there"
                        "are no input files, it can also be used in "
                        "conjunction with input files (default=None)")
    parser.add_argument('-g', '--inglob', type=str,
                        help="An input directory glob that is used in "
                        "conjunction with the --indir, the default is no "
                        "pattern")
    parser.add_argument('-o', '--outfile', type=str,
                        help="The output file, if not provided, output is to "
                        "STDOUT, default is to STDOUT")
    parser.add_argument('-c', '--columns', type=str, nargs='+',
                        help="Specific column names to use from the files (if"
                        " they all have the same header) or groups of column"
                        " names that contain the same data, groups should be"
                        " given in the format:"
                        " 'output_col_name=input_col1,input_col2,input_colX"
                        "...' with no sapces and may need quoting.")
    parser.add_argument('-r', '--regexp', type=str,
                        help="A regular expression used to generate the "
                        "ID column from the full input file path. The"
                        " regexp will be applied using re.search. All groups"
                        " will be captured, although the groups required in "
                        "the ID should be labeled 'A', 'B', 'C' using the "
                        "named capure groups `(?P<C>)` etc... . "
                        "Remember to also use non-capturing groups `(?:)` "
                        "where necessary.")
    parser.add_argument('-d', '--delimiter', type=str,
                        help="The delimiter of the input files. Only use if "
                        "all input files have the same delimiter. If not, "
                        "the delimiter is sniffed using csv.Sniffer "
                        "the default)")
    parser.add_argument('-t', '--out-delimiter', type=str, default="\t",
                        help="The delimiter of the output file (or STDOUT)."
                        " (default='\t')")
    parser.add_argument('-m', '--md5row', action='store_true',
                        help="Add an md5 for each row. The MD5 is created from"
                        " a pipe '|' concatination of each row. This can be "
                        "useful if you want to unique on rows after merging"
                        " (default=False)")
    parser.add_argument('-e', '--delete', action='store_true',
                        help="Delete the merged files (default=False)")
    parser.add_argument('-u', '--unsafe', action='store_true',
                        help="Delete the merged files after each merge, this "
                        "could be potentially unsafe as there could be a "
                        "failue during the merge that will leave everything "
                        "in an unfinished state. However, various header "
                        "checks are done before merging and each file is "
                        "read through first before merging to make sure that "
                        "there are no issues before merging, so this option "
                        "makes everything really slow but can be useful if you"
                        " have limited disk space (default=False)")
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='Shall I print progress to STDERR?')
    return parser


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def parse_cmd_line_args(parser):
    """
    Parse and process the command line arguments that have been defined
    using argparse. This will fail if there is no `delimiter`, `columns`,
    `delete`, `unsafe` or `regexp` argument, as they are all processed within
    it.

    Parameters
    ----------
    args : :obj:`argparse.ArgumentParser`
        The parser with at least `delimiter`, `columns`, `delete`, `unsafe` or
        `regexp` arguments set

    Returns
    -------
    args : :obj:`Namespace`
        The arguments from parsing the cmd line args
    """
    args = parser.parse_args()

    # Make a literal tab into a python tab, just in case the user passes '\t'
    # on the command line and not $'\t'
    if args.delimiter == r'\t':
        args.delimiter = "\t"

    # If a delimiter has been supplied then we convert it into a dialect
    if args.delimiter is not None:
        args.delimiter = csv.Dialect(delimiter=args.delimiter)

    # Make sure that all the columns are parsed correctly
    args.columns = parse_column_arg(args.columns)

    # Make sure that unsafe has not been specified when delete has not been
    # sepecified - I can probably do this with argparse options
    check_delete_args(args.delete, args.unsafe)

    # Compile the regular expression
    args.regexp = set_regexp(args.regexp)

    return args


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def set_regexp(regexp):
    """
    Compile the regexp that has been passed on the command line.

    Parameters
    ----------
    regexp :str or NoneType, optional
        A regular expression to compile or Nonetype. If it is NoneType, then
        it is simply returned, if not then we attempt to compile it

    Returns
    -------
    compiled_regexp : :obj:`re.Pattern`
        A compiled regexp. Note that no flags are added to the compilation
    """
    if regexp is not None:
        regexp = r'{0}'.format(regexp)
        return re.compile(regexp)
    return regexp


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def parse_column_arg(col_arg):
    """
    Parse the column args into the correct format of a `list` of `str` or
    `tuple`

    Parameters
    ----------
    col_arg : list of str or NoneType
        This is the columns that have been parsed in from the command line.
        Each element will either be a single string or a column proxy with the"
        " format: `output_col_name=input_col1,input_col2,input_colX...`. It
        could also be NoneType, in which case it is passed through.

    Return
    ------
    columns : NoneType or list of str or tuple
        A subset of columns that are required to be output, rather than
        all of the columns. If None, then all columns are output and the
        headers of each file must be the same. If defined, then the header
        can appear in any order. If the list element is a str, then it is
        assumed that the column name must appear in all files. If it is a
        tuple, then the first element is the output column name in the
        header and the second element should be another tuple of possible
        column names that will appear in each file. It is assumed that only
        one of the possible column names should appear in each file and it
        is an error if > than one appears (default=NoneType)

    Raises
    ------
    ValueError
        If the column specification format does not look good
    """
    columns = []
    try:
        for i in col_arg:
            # We force the element to be a string, just in case
            i = str(i)

            # here element 0 will be the output column name and element 1 will
            # be the input column names. We only split the first occurrence
            # just in case there are = in the input column names
            args = i.split('=', maxsplit=1)

            # If there is a single argument it means that we are selecting a
            # single column that we expect to be in all output files
            if len(args) == 1:
                columns.append(args[0].strip())
            elif len(args[1]) == 2:
                # We have some column proxies, so we go on to split them on
                # commas and remove any leading/lagging whitespace that could
                # be present
                columns.append((args[0].strip(),
                                tuple([c.strip() for c in args[1].split(',')])))
            else:
                # I doubt this will ever get raised
                raise ValueError("unknown column format '{0}'".format(i))
    except TypeError:
        # col_arg is probably NoneType
        return None


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def check_delete_args(delete_arg, unsafe_arg):
    """
    Make sure the combination of the delele and unsafe arguments is valid

    Parameters
    ----------
    delete_arg :bool
        The file delete argument
    unsafe_arg :bool
        The file delete unsafe argument. This is only active if `delete_arg` is
        `True`, and it is an error if not

    Raises
    ------
    ValueError
        If `delete_arg` is `False` and `unsafe_arg` is `True`
    """
    if delete_arg is False and unsafe_arg is True:
        raise ValueError("unsafe argument is only active when delete argument"
                         " is True")


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_default_message(stacker):
    """
    Generate the default animation message
    """
    return "processing file {0}/{1}: {2}".format(
        stacker._curr_file_idx+1,
        len(stacker._log),
        os.path.basename(stacker.curr_file_name))


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def main():
    """
    The main entry point for the script
    """
    parser = init_cmd_line_args()
    args = parse_cmd_line_args(parser)
    msg = progress.Msg(verbose=args.verbose, file=sys.stderr)
    msg.msg('=== stacker ({0} v{1}) ==='.format(PKG_NAME, __version__))
    msg.prefix = '[info]'

    # Now get the input files to merge
    args.infiles = file_helper.gather_input_files(infiles=args.infiles,
                                                  indir=args.indir,
                                                  inglob=args.inglob)
    msg.msg_atts(args)

    try:
        # Determine where we want the temp directory
        temp_loc = os.path.dirname(os.path.expanduser(args.outfile))
    except TypeError:
        # outfile is NoneType so we are outputting to STDOUT
        temp_loc = None

           # writer.writerow(row)


    # The flexiwriter will control all the tempoutput or STDOUT output
    with file_helper.FlexiWriter(args.outfile, delimiter=args.out_delimiter,
                                 lineterminator=os.linesep,
                                 dir=temp_loc, prefix='.') as writer:
        # The FIleStack object will outout the files
        with FileStack(args.infiles, columns=args.columns,
                       dialect=args.delimiter, delete=args.delete,
                       unsafe=args.unsafe, md5row=args.md5row,
                       regexp_id=args.regexp) as stacker:
            # Setup the progress monitor, if the unsafe option is active then
            # we will have a row count after the initiialisation, so can use
            # a remaining progress monitor, if not we make do with a
            # RateProgress monitor
            prog = None
            if args.unsafe is True:
                prog = progress.RemainingProgress(nlines=stacker.nlines,
                                                  verbose=args.verbose,
                                                  file=sys.stderr)
            else:
                prog = progress.RateProgress(verbose=args.verbose,
                                             file=sys.stderr)

            # output the header
            writer.writerow(stacker.header)

            cur_idx = 0
            prog.default_msg = get_default_message(stacker)
            # Now loop through and get the data
            for row in prog.progress(stacker):
                if stacker._curr_file_idx != cur_idx:
                    prog.default_msg = get_default_message(stacker)
                    cur_idx = stacker._curr_file_idx
                writer.writerow(row)


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
if __name__ == '__main__':
    main()
