"""Commands is a core of the perun implementation containing the basic commands.

Commands contains implementation of the basic commands of perun pcs. It is meant
to be run both from GUI applications and from CLI, where each of the function is
possible to be run in isolation.
"""

import collections
import os
import re

from operator import itemgetter

import colorama

import perun.logic.pcs as pcs
import perun.logic.config as perun_config
import perun.logic.store as store
import perun.logic.index as index
import perun.profile.helpers as profile
import perun.utils as utils
import perun.utils.helpers as helpers
import perun.utils.log as perun_log
import perun.utils.timestamps as timestamp
import perun.vcs as vcs
import perun.logic.temp as temp
import perun.logic.stats as stats

from perun.utils.exceptions import NotPerunRepositoryException, \
    ExternalEditorErrorException, MissingConfigSectionException, InvalidTempPathException, \
    ProtectedTempException
from perun.utils.helpers import \
    TEXT_EMPH_COLOUR, TEXT_ATTRS, TEXT_WARN_COLOUR, \
    PROFILE_TYPE_COLOURS, SUPPORTED_PROFILE_TYPES, HEADER_ATTRS, HEADER_COMMIT_COLOUR, \
    HEADER_INFO_COLOUR, HEADER_SLASH_COLOUR, PROFILE_DELIMITER, MinorVersion
from perun.utils.log import cprint, cprintln
from perun.utils.structs import ProfileListConfig

# Init colorama for multiplatform colours
colorama.init()
UNTRACKED_REGEX = \
    re.compile(r"([^\\]+)-([0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}).perf")
# Regex for parsing the formating tag [<tag>:<size>f<fill_char>]
FMT_REGEX = re.compile(r"%([a-zA-Z]+)(:[0-9]+)?(f.)?%")

def config_get(store_type, key):
    """Gets from the store_type configuration the value of the given key.

    :param str store_type: type of the store lookup (local, shared of recursive)
    :param str key: list of section delimited by dot (.)
    """
    config_store = pcs.global_config() if store_type in ('shared', 'global') else pcs.local_config()

    if store_type == 'recursive':
        value = perun_config.lookup_key_recursively(key)
    else:
        value = config_store.get(key)
    perun_log.info("{}: {}".format(key, value))

def config_set(store_type, key, value):
    """Sets in the store_type configuration the key to the given value.

    :param str store_type: type of the store lookup (local, shared of recursive)
    :param str key: list of section delimited by dot (.)
    :param object value: arbitrary value that will be set in the configuration
    """
    config_store = pcs.global_config() if store_type in ('shared', 'global') else pcs.local_config()

    config_store.set(key, value)
    perun_log.info("Value '{1}' set for key '{0}'".format(key, value))

def config_edit(store_type):
    """Runs the external editor stored in general.editor key in order to edit the config file.

    :param str store_type: type of the store (local, shared, or recursive)
    :raises MissingConfigSectionException: when the general.editor is not found in any config
    :raises ExternalEditorErrorException: raised if there are any problems during invoking of
        external editor during the 'edit' operation
    """
    # Lookup the editor in the config and run it as external command
    editor = perun_config.lookup_key_recursively('general.editor')
    config_file = pcs.get_config_file(store_type)
    try:
        utils.run_external_command([editor, config_file])
    except Exception as inner_exception:
        raise ExternalEditorErrorException(editor, str(inner_exception))

def config_reset(store_type, config_template):
    """Resets the given store_type to a default type (or to a selected configuration template)

    For more information about configuration templates see :ref:`config-templates`.

    :param str store_type: name of the store (local or global) which we are resetting
    :param str config_template: name of the template that we are resetting to
    :raises NotPerunRepositoryException: raised when we are outside of any perun scope
    """
    if store_type in ('shared', 'global'):
        shared_location = perun_config.lookup_shared_config_dir()
        perun_config.init_shared_config_at(shared_location)
    else:
        vcs_config = {
            'vcs': {
                'url': pcs.get_vcs_path(),
                'type': pcs.get_vcs_type()
            }
        }
        perun_config.init_local_config_at(pcs.get_path(), vcs_config, config_template)
    perun_log.info("{} configuration reset{}".format(
        'global' if store_type in ('shared', 'global') else 'local',
        " to {}".format(config_template) if store not in ("shared", "global") else ""
    ))

def init_perun_at(perun_path, is_reinit, vcs_config, config_template='master'):
    """Initialize the .perun directory at given path

    Initializes or reinitializes the .perun directory at the given path.

    :param path perun_path: path where new perun performance control system will be stored
    :param bool is_reinit: true if this is existing perun, that will be reinitialized
    :param dict vcs_config: dictionary of form {'vcs': {'type', 'url'}} for local config init
    :param str config_template: name of the configuration template
    """
    # Initialize the basic structure of the .perun directory
    perun_full_path = os.path.join(perun_path, '.perun')
    helpers.touch_dir(perun_full_path)
    helpers.touch_dir(os.path.join(perun_full_path, 'objects'))
    helpers.touch_dir(os.path.join(perun_full_path, 'jobs'))
    helpers.touch_dir(os.path.join(perun_full_path, 'logs'))
    helpers.touch_dir(os.path.join(perun_full_path, 'cache'))
    helpers.touch_dir(os.path.join(perun_full_path, 'stats'))
    helpers.touch_dir(os.path.join(perun_full_path, 'tmp'))
    # If the config does not exist, we initialize the new version
    if not os.path.exists(os.path.join(perun_full_path, 'local.yml')):
        perun_config.init_local_config_at(perun_full_path, vcs_config, config_template)
    else:
        perun_log.info('configuration file already exists. Run ``perun config reset`` to reset'
                       ' configuration to default state.')

    # Perun successfully created
    msg_prefix = "Reinitialized existing" if is_reinit else "Initialized empty"
    perun_log.msg_to_stdout(msg_prefix + " Perun repository in {}".format(perun_path), 0)

def init(dst, configuration_template='master', **kwargs):
    """Initializes the performance and version control systems

    Inits the performance control system at a given directory. Optionally inits the
    wrapper of the Version Control System that is used as tracking point.

    :param path dst: path where the pcs will be initialized
    :param dict kwargs: keyword arguments of the initialization
    :param str configuration_template: name of the template that will be used for initialization
        of local configuration
    """
    perun_log.msg_to_stdout("call init({}, {})".format(dst, kwargs), 2)

    # First init the wrapping repository well
    vcs_type = kwargs['vcs_type']
    vcs_path = kwargs['vcs_path'] or dst
    vcs_params = kwargs['vcs_params']

    # Construct local config
    vcs_config = {
        'vcs': {
            'url': vcs_path,
            'type': vcs_type
        }
    }

    # Check if there exists perun directory above and initialize the new pcs
    try:
        super_perun_dir = helpers.locate_perun_dir_on(dst)
        is_pcs_reinitialized = (super_perun_dir == dst)
        if not is_pcs_reinitialized:
            perun_log.warn("There exists super perun directory at {}".format(super_perun_dir))
    except NotPerunRepositoryException:
        is_pcs_reinitialized = False

    init_perun_at(dst, is_pcs_reinitialized, vcs_config, configuration_template)

    # If the wrapped repo could not be initialized we end with error. The user should adjust this
    # himself and fix it in the config. Note that this decision was made after tagit design,
    # where one needs to further adjust some options in initialized directory.
    if vcs_type and not vcs.init(vcs_params):
        err_msg = "Could not initialize empty {0} repository at {1}.\n".format(vcs_type, vcs_path)
        err_msg += "Either reinitialize perun with 'perun init' or initialize {0} repository"
        err_msg += "manually and fix the path to vcs in 'perun config --edit'"
        perun_log.error(err_msg)

@vcs.lookup_minor_version
def add(profile_names, minor_version, keep_profile=False, force=False):
    """Appends @p profile to the @p minor_version inside the @p pcs

    :param generator profile_names: generator of profiles that will be stored for the minor version
    :param str minor_version: SHA-1 representation of the minor version
    :param bool keep_profile: if true, then the profile that is about to be added will be not
        deleted, and will be kept as it is. By default false, i.e. profile is deleted.
    :param bool force: if set to true, then the add will be forced, i.e. the check for origin will
        not be performed.
    """
    added_profile_count = 0
    for profile_name in profile_names:
        # Test if the given profile exists (This should hold always, or not?)
        if not os.path.exists(profile_name):
            perun_log.error("profile {} does not exists".format(profile_name), recoverable=True)
            continue

        # Load profile content
        # Unpack to JSON representation
        unpacked_profile = store.load_profile_from_file(profile_name, True)

        if not force and unpacked_profile['origin'] != minor_version:
            error_msg = "cannot add profile '{}' to minor index of '{}':".format(
                profile_name, minor_version
            )
            error_msg += "profile originates from minor version '{}'".format(
                unpacked_profile['origin']
            )
            perun_log.error(error_msg, recoverable=True)
            continue

        # Remove origin from file
        unpacked_profile.pop('origin')
        profile_content = profile.to_string(unpacked_profile)

        # Append header to the content of the file
        header = "profile {} {}\0".format(unpacked_profile['header']['type'], len(profile_content))
        profile_content = (header + profile_content).encode('utf-8')

        # Transform to internal representation - file as sha1 checksum and content packed with zlib
        profile_sum = store.compute_checksum(profile_content)
        compressed_content = store.pack_content(profile_content)

        # Add to control
        object_dir = pcs.get_object_directory()
        store.add_loose_object_to_dir(object_dir, profile_sum, compressed_content)

        # Register in the minor_version index
        index.register_in_minor_index(
            object_dir, minor_version, profile_name, profile_sum, unpacked_profile
        )

        # Remove the file
        if not keep_profile:
            os.remove(profile_name)

        added_profile_count += 1

    profile_names_len = len(profile_names)
    if added_profile_count != profile_names_len:
        perun_log.error("could not register {} in index: {} failed".format(
            helpers.str_to_plural(added_profile_count, "profile"),
            added_profile_count - profile_names_len
        ))
    perun_log.info("successfully registered {} profiles in index".format(added_profile_count))

@vcs.lookup_minor_version
def remove_from_index(profile_generator, minor_version):
    """Removes @p profile from the @p minor_version inside the @p pcs

    :param generator profile_generator: profile that will be stored for the minor version
    :param str minor_version: SHA-1 representation of the minor version
    :raisesEntryNotFoundException: when the given profile_generator points to non-tracked profile
    """
    object_directory = pcs.get_object_directory()
    index.remove_from_index(object_directory, minor_version, profile_generator)

def remove_from_pending(profile_generator):
    """Removes profiles from the pending jobs directory (i.e, `.perun/jobs`

    :param generator profile_generator: generator of profiles that will be removed from pending jobs
    """
    removed_profile_number = len(profile_generator)
    for i, pending_file in enumerate(profile_generator):
        os.remove(pending_file)
        perun_log.info("{}/{} deleted {} from pending jobs".format(
            helpers.format_counter_number(i+1, removed_profile_number),
            removed_profile_number,
            perun_log.in_color(os.path.split(pending_file)[1], 'grey'),
        ))

    if removed_profile_number:
        result_string = perun_log.in_color("{}".format(
            helpers.str_to_plural(removed_profile_number, "profile")
        ), 'white', 'bold')
        perun_log.info("successfully removed {} from pending jobs".format(
            result_string
        ))

def calculate_profile_numbers_per_type(profile_list):
    """Calculates how many profiles of given type are in the profile type.

    Returns dictionary mapping types of profiles (i.e. memory, time, ...) to the
    number of profiles that are occurring in the profile list. Used for statistics
    about profiles corresponding to minor versions.

    :param list profile_list: list of ProfileInfo with information about profiles
    :returns dict: dictionary mapping profile types to number of profiles of given type in the list
    """
    profile_numbers = collections.defaultdict(int)
    for profile_info in profile_list:
        profile_numbers[profile_info.type] += 1
    profile_numbers['all'] = len(profile_list)
    return profile_numbers

def print_profile_numbers(profile_numbers, profile_types, line_ending='\n'):
    """Helper function for printing the numbers of profile to output.

    :param dict profile_numbers: dictionary of number of profiles grouped by type
    :param str profile_types: type of the profiles (tracked, untracked, etc.)
    :param str line_ending: ending of the print (for different outputs of log and status)
    """
    if profile_numbers['all']:
        print("{0[all]} {1} profiles (".format(profile_numbers, profile_types), end='')
        first_printed = False
        for profile_type in SUPPORTED_PROFILE_TYPES:
            if not profile_numbers[profile_type]:
                continue
            print(', ' if first_printed else '', end='')
            first_printed = True
            type_colour = PROFILE_TYPE_COLOURS[profile_type]
            cprint("{0} {1}".format(profile_numbers[profile_type], profile_type), type_colour)
        print(')', end=line_ending)
    else:
        cprintln('(no {} profiles)'.format(profile_types), TEXT_WARN_COLOUR, attrs=TEXT_ATTRS)

def turn_off_paging_wrt_config(paged_function):
    """Helper function for checking if the function should be paged or not according to the config
    setting ``general.paging``.

    If in global config the ``genera.paging`` is set to ``always``, then any function should be
    paged. Otherwise we check if ``general.paging`` contains either ``only-log`` or ``only-status``.

    :param str paged_function: name of the paged function, which will be looked up in config
    :return: true if the function should be paged (unless --no-pager is set)
    """
    try:
        paging_option = perun_config.shared().get('general.paging')
    # Test for backward compatibility with old instances of Perun and possible issues
    except MissingConfigSectionException:
        perun_log.warn("""corrupted shared configuration file: missing ``general.paging`` option.

Run ``perun config --shared --edit`` and set the ``general.paging`` to one of following:
    always, only-log, only-status, never

Consult the documentation (Configuration and Logs) for more information about paging of
output of status, log and others.
        """)
        return True
    return paging_option == 'always' or \
        (paging_option.startswith('only-') and paging_option.endswith(paged_function))

@perun_log.paged_function(paging_switch=turn_off_paging_wrt_config('log'))
@vcs.lookup_minor_version
def log(minor_version, short=False, **_):
    """Prints the log of the performance control system

    Either prints the short or longer version. In short version, only header and short
    list according to the formatting string from stored in the configuration. Prints
    the number of profiles associated with each of the minor version and some basic
    information about minor versions, like e.g. description, hash, etc.

    :param str minor_version: representation of the head version
    :param bool short: true if the log should be in short format
    """
    perun_log.msg_to_stdout("Running inner wrapper of the 'perun log '", 2)

    # Print header for --short-minors
    if short:
        minor_versions = list(vcs.walk_minor_versions(minor_version))
        # Reduce the descriptions of minor version to one liners
        for mv_no, minor in enumerate(minor_versions):
            minor_versions[mv_no] = minor._replace(desc=minor.desc.split("\n")[0])
        minor_version_maxima = calculate_maximal_lengths_for_object_list(
            minor_versions, MinorVersion._fields
        )
        # Update manually the maxima for the printed supported profile types, each requires two
        # characters and 9 stands for " profiles" string

        def minor_stat_retriever(minor_v):
            """Helper function for picking stats of the given minor version

            :param MinorVersion minor_v: minor version for which we are retrieving the stats
            :return: dictionary with stats for minor version
            """
            return index.get_profile_number_for_minor(
                pcs.get_object_directory(), minor_v.checksum
            )

        def deg_count_retriever(minor_v):
            """Helper function for picking stats of the degradation strings of form ++--

            :param MinorVersion minor_v: minor version for which we are retrieving the stats
            :return: dictionary with stats for minor version
            """
            counts = perun_log.count_degradations_per_group(
                store.load_degradation_list_for(pcs.get_object_directory(), minor_v.checksum)
            )
            return {'changes': counts.get('Optimization', 0)*'+' + counts.get('Degradation', 0)*'-'}

        minor_version_maxima.update(
            calculate_maximal_lengths_for_stats(minor_versions, minor_stat_retriever)
        )
        minor_version_maxima.update(
            calculate_maximal_lengths_for_stats(minor_versions, deg_count_retriever, " changes ")
        )
        print_shortlog_minor_version_info_list(minor_versions, minor_version_maxima)
    else:
        # Walk the minor versions and print them
        for minor in vcs.walk_minor_versions(minor_version):
            cprintln("Minor Version {}".format(minor.checksum), TEXT_EMPH_COLOUR, attrs=TEXT_ATTRS)
            base_dir = pcs.get_object_directory()
            tracked_profiles = index.get_profile_number_for_minor(base_dir, minor.checksum)
            print_profile_numbers(tracked_profiles, 'tracked')
            print_minor_version_info(minor, indent=1)

def adjust_limit(limit, attr_type, maxima, padding=0):
    """Returns the adjusted value of the limit for the given field output in status or log

    Takes into the account the limit, which is specified in the field (e.g. as checksum:6),
    the maximal values of the given fields. Additionally, one can add a padding, e.g. for
    the type tag.

    :param match limit: matched string from the formatting string specifying the limit
    :param str attr_type: string name of the attribute, which we are limiting
    :param dict maxima: maximas of values of given attribute types
    :param int padding: additional padding of the limit not contributed to maxima
    :return: adjusted value of the limit w.r.t. attribute type and maxima
    """
    return max(int(limit[1:]), len(attr_type)) if limit else maxima[attr_type] + padding

def print_shortlog_minor_version_info_list(minor_version_list, max_lengths):
    """Prints list of profiles and counts per type of tracked/untracked profiles.

    Prints the list of profiles, trims the sizes of each information according to the
    computed maximal lengths If the output is short, the list itself is not printed,
    just the information about counts. Tracked and untracked differs in colours.

    Example of the output:

    checksum ( a|m|x|t profiles)                  desc                        changes
    aac4d21a (24|0|0|0 profiles) Bump version and changelog to 0.17.2
    91373c43 ( 2|0|0|2 profiles) Bump version and changelog to 0.16.8

    :param list minor_version_list: list of profiles of MinorVersionInfo objects
    :param dict max_lengths: dictionary with maximal sizes for the output of profiles
    """

    # Load formating string for profile
    minor_version_info_fmt = perun_config.lookup_key_recursively('format.shortlog')
    fmt_tokens = perun_log.scan_formatting_string(
        minor_version_info_fmt, {}, default_fmt_callback=lambda token: "%" + token + "%"
    )

    # Print header (2 is padding for id), e.g.:
    # checksum ( a|m|x|t profiles)                  desc                        changes
    print_shortlog_profile_list_header(fmt_tokens, max_lengths)

    # Print profiles, e.g.:
    # aac4d21a (24|0|0|0 profiles) Bump version and changelog to 0.17.2
    # 91373c43 ( 2|0|0|2 profiles) Bump version and changelog to 0.16.8
    print_shortlog_profile_list(fmt_tokens, max_lengths, minor_version_info_fmt, minor_version_list)

def print_shortlog_profile_list(tokens, max_lengths, fmt_string, minor_versions):
    """For each minor versions, prints the stats w.r.t to the formatting tokens specified in
    @p tokens.

    Iterates through all of the minor versions, and then outputs one row according to the formatting
    tokens and their values in the specified versions. Each column is adjusted according to its
    maximal widths.

    The example of output is:

    aac4d21a (24|0|0|0 profiles) Bump version and changelog to 0.17.2
    91373c43 ( 2|0|0|2 profiles) Bump version and changelog to 0.16.8
    <token->  <-----token----->  <--------------token--------------->

    :param list tokens: list of formatting tokens
    :param dict max_lengths: dictionary mapping the maximal lengths of each value corresponding to
        column of the formatting token
    :param str fmt_string: formatting string
    :param list minor_versions: list of profiles of MinorVersionInfo objects
    """
    stat_length = sum([
        max_lengths['all'], max_lengths['time'], max_lengths['mixed'], max_lengths['memory']
    ]) + 3 + len(" profiles")

    for minor_version in minor_versions:
        for (token_type, token) in tokens:
            if token_type == 'fmt_string':
                print_shortlog_token(fmt_string, max_lengths, minor_version, stat_length, token)
            # Non-token parts of the formatting string are printed as they are
            else:
                cprint(token, 'white')
        perun_log.info("")

def print_shortlog_token(fmt_string, max_lengths, minor_version, stat_len, token):
    """Prints token of the formatting string.

    Example of tokens are highlighted below:

    aac4d21a (24|0|0|0 profiles) Bump version and changelog to 0.17.2
    91373c43 ( 2|0|0|2 profiles) Bump version and changelog to 0.16.8
    <token->  <-----token----->  <--------------token--------------->

    :param dict max_lengths: dictionary mapping the maximal lengths of each value corresponding to
        column of the formatting token
    :param str fmt_string: formating string
    :param MinorVersionInfo minor_version: MinorVersionInfo objects
    :param int stat_len: the whole length of the formatting header
    :param string token: one given token of formatting string
    """
    attr_type, limit, fill = FMT_REGEX.match(token).groups()
    limit = max(int(limit[1:]), len(attr_type)) if limit else max_lengths[attr_type]
    if attr_type == 'stats':
        # (24|0|0|0 profiles)
        print_stats_token(max_lengths, minor_version, stat_len)
    elif attr_type == 'changes':
        # +++---
        print_changes_token(max_lengths, minor_version)
    else:
        # "91373c43",  "Bump version and changelog to 0.16.8"
        print_other_formatting_string(
            fmt_string, minor_version, attr_type, limit, value_fill=fill or ' '
        )

def print_changes_token(max_lengths, minor_version):
    """Prints information about changes in the minor version, i.e. optimizations and degradations.

    The example of changes token is: "+++---"

    :param dict max_lengths: dictionary mapping the maximal lengths of each value corresponding to
        column of the formatting token
    :param MinorVersionInfo minor_version: MinorVersionInfo objects
    """
    degradations = store.load_degradation_list_for(
        pcs.get_object_directory(), minor_version.checksum
    )
    change_string = perun_log.change_counts_to_string(
        perun_log.count_degradations_per_group(degradations),
        width=max_lengths['changes']
    )
    perun_log.info(change_string, end='')

def print_stats_token(max_lengths, minor_version, stat_length):
    """Prints the statistic of profiles for the given minor versions.

    The example of stats token is: "(24|0|0|0 profiles)"

    :param dict max_lengths: dictionary mapping the maximal lengths of each value corresponding to
        column of the formatting token
    :param MinorVersionInfo minor_version: MinorVersionInfo objects
    :param int stat_length: the whole length of the formatting header
    """
    tracked_profiles = index.get_profile_number_for_minor(
        pcs.get_object_directory(), minor_version.checksum
    )
    if tracked_profiles['all']:
        perun_log.info(perun_log.in_color("{:{}}".format(
            tracked_profiles['all'], max_lengths['all']
        ), TEXT_EMPH_COLOUR, TEXT_ATTRS), end='')

        # Print the coloured numbers
        for profile_type in SUPPORTED_PROFILE_TYPES:
            perun_log.info("{}{}".format(
                perun_log.in_color(PROFILE_DELIMITER, HEADER_SLASH_COLOUR),
                perun_log.in_color("{:{}}".format(
                    tracked_profiles[profile_type], max_lengths[profile_type]
                ), PROFILE_TYPE_COLOURS[profile_type])
            ), end='')

        perun_log.info(
            perun_log.in_color(
                " profiles", HEADER_INFO_COLOUR, TEXT_ATTRS
            ), end=''
        )
    else:
        perun_log.info(
            perun_log.in_color(
                '--no--profiles--'.center(stat_length), TEXT_WARN_COLOUR, TEXT_ATTRS
            ), end=''
        )

def print_shortlog_profile_list_header(fmt_tokens, max_lengths):
    """Prints the header of the output of the minor version information

    The example of shortlog header is:

    checksum ( a|m|x|t profiles)                  desc                        changes

    :param list fmt_tokens: list of formatting tokens
    :param dict max_lengths: dictionary of maximal values of columns corresponding to the tokens
    """
    for (token_type, token) in fmt_tokens:
        if token_type == 'fmt_string':
            attr_type, limit, _ = FMT_REGEX.match(token).groups()
            if attr_type == 'stats':
                print_shortlog_stats_header(max_lengths)
            else:
                limit = adjust_limit(limit, attr_type, max_lengths)
                token_string = attr_type.center(limit, ' ')
                cprint(token_string, 'white', HEADER_ATTRS)
        else:
            # Print the rest (non token stuff)
            cprint(token, 'white', HEADER_ATTRS)
    perun_log.info("")

def print_shortlog_stats_header(max_lengths):
    """Prints header for the stats, adjusted according to the lengths of each profile info

    The stats header is in form of: a|m|x|t profiles

    :param dict max_lengths: dictionary that computes the maximal lengths of each column
    """
    slash = perun_log.in_color(PROFILE_DELIMITER, HEADER_SLASH_COLOUR, HEADER_ATTRS)
    end_msg = perun_log.in_color(' profiles', HEADER_SLASH_COLOUR, HEADER_ATTRS)
    perun_log.info(perun_log.in_color("{0}{4}{1}{4}{2}{4}{3}{5}".format(
        perun_log.in_color(
            'a'.rjust(max_lengths['all']), HEADER_COMMIT_COLOUR, HEADER_ATTRS
        ),
        perun_log.in_color(
            'm'.rjust(max_lengths['memory']),
            PROFILE_TYPE_COLOURS['memory'], HEADER_ATTRS
        ),
        perun_log.in_color(
            'x'.rjust(max_lengths['mixed']),
            PROFILE_TYPE_COLOURS['mixed'], HEADER_ATTRS),
        perun_log.in_color(
            't'.rjust(max_lengths['time']),
            PROFILE_TYPE_COLOURS['time'], HEADER_ATTRS),
        slash,
        end_msg
    ), HEADER_SLASH_COLOUR, HEADER_ATTRS), end='')

def print_minor_version_info(head_minor_version, indent=0):
    """Prints the information about given minor version both in log and status

    In particular, it lists the author, email, date, parents and description.
    Example of minor version info is:

    Author: Tomas Fiedor <ifiedortom@fit.vutbr.cz> 2021-02-18 12:21:35
    Parent: 7b5ec3496c0c4b5b048950ed230e7084e511938c

    Refactor commands and cli_helpers

    :param MinorVersion head_minor_version: identification of the commit (preferably sha1)
    :param int indent: indent of the description part
    """
    perun_log.info("Author: {0.author} <{0.email}> {0.date}".format(head_minor_version))
    for parent in head_minor_version.parents:
        perun_log.info("Parent: {}".format(parent))
    perun_log.info("")
    indented_desc = '\n'.join(map(
        lambda line: ' '*(indent*4) + line, head_minor_version.desc.split('\n')
    ))
    perun_log.info(indented_desc)

def print_other_formatting_string(fmt_string, info_object, info_attr, size_limit,
                                  colour='white', value_fill=' '):
    """Prints the token from the fmt_string, according to the values stored in info_object

    info_attr is one of the tokens from fmt_string, which is extracted from the info_object,
    that stores the real value. This value is then output to stdout with colours, fills,
    and is trimmed to the given size.

    :param str fmt_string: formatting string for the given token
    :param object info_object: object with stored information (ProfileInfo or MinorVersion)
    :param int size_limit: will limit the output of the value of the info_object to this size
    :param str info_attr: attribute we are looking up in the info_object
    :param str colour: default colour of the formatting token that will be printed out
    :param char value_fill: will fill the string with this
    """
    # Check if encountered incorrect token in the formatting string
    if not hasattr(info_object, info_attr):
        perun_log.error(
            "invalid formatting string '{}': object does not contain '{}' attribute".format(
                fmt_string, info_attr
            )
        )

    # Obtain the value for the printing
    raw_value = getattr(info_object, info_attr)
    info_value = raw_value[:size_limit].ljust(size_limit, value_fill)

    # Print the actual token
    if info_attr == 'type':
        cprint("[{}]".format(info_value), PROFILE_TYPE_COLOURS[raw_value])
    else:
        cprint(info_value, colour)

def calculate_maximal_lengths_for_stats(obj_list, stat_function, stat_header=""):
    """For given object lists and stat_function compute maximal lengths of the stats

    :param list obj_list: list of object, for which the stat function will be applied
    :param function stat_function: function returning the dictionary of keys
    :param str stat_header: header of the stats
    :return: dictionary of maximal lenghts for various stats
    """
    maxima = collections.defaultdict(int)
    for obj in obj_list:
        object_stats = stat_function(obj)
        for key in object_stats.keys():
            maxima[key] = max(len(stat_header), maxima[key], len(str(object_stats[key])))
    return maxima

def calculate_maximal_lengths_for_object_list(object_list, valid_attributes):
    """For given object list, will calculate the maximal sizes for its values for table view.

    :param list object_list: list of objects (e.g. ProfileInfo or MinorVersion) information
    :param list valid_attributes: list of valid attributes of objects from list
    :returns dict: dictionary with maximal lengths for profiles
    """
    # Measure the maxima for the lengths of the object info
    max_lengths = collections.defaultdict(int)
    for object_info in object_list:
        for attr in valid_attributes:
            if hasattr(object_info, attr):
                max_lengths[attr] \
                    = max(len(attr), max_lengths[attr], len(str(getattr(object_info, attr))))
    return max_lengths

def print_status_profile_list(profiles, max_lengths, short, list_type='tracked'):
    """Prints list of profiles and counts per type of tracked/untracked profiles.

    Prints the list of profiles, trims the sizes of each information according to the
    computed maximal lengths If the output is short, the list itself is not printed,
    just the information about counts. Tracked and untracked differs in colours.
    The example of output is as follows:

    17 untracked profiles (6 memory, 8 mixed, 3 time):

    ═════════════════════════════════════════════════════════════════════════▣
      id  ┃   type   ┃                         source                        ┃
    ═════════════════════════════════════════════════════════════════════════▣
      0@p ┃ [time  ] ┃ time-example.perf                                     ┃
    ═════════════════════════════════════════════════════════════════════════▣
      1@p ┃ [mixed ] ┃ complexity-quicksort-[_]-[_]-2018-03-26-13-52-58.perf ┃
      2@p ┃ [memory] ┃ memory-mct-[_]-[_]-2018-03-26-12-16-36.perf           ┃
      3@p ┃ [mixed ] ┃ complexity-gif2bmp-[_]-[_]-2018-03-26-12-12-15.perf   ┃
      4@p ┃ [memory] ┃ memory-mct-[_]-[_]-2018-03-26-10-07-47.perf           ┃
      5@p ┃ [mixed ] ┃ complexity-quicksort-[_]-[_]-2018-03-22-17-04-52.perf ┃
    ═════════════════════════════════════════════════════════════════════════▣

    :param list profiles: list of profiles of ProfileInfo objects
    :param dict max_lengths: dictionary with maximal sizes for the output of profiles
    :param bool short: true if the output should be short
    :param str list_type: type of the profile list (either untracked or tracked)
    """
    # Sort the profiles w.r.t time of creation
    list_config = ProfileListConfig(list_type, short, profiles)
    profile.sort_profiles(profiles)

    # Print with padding
    profile_numbers = calculate_profile_numbers_per_type(profiles)
    print_profile_numbers(profile_numbers, list_type, list_config.ending)

    # Skip empty profile list or shortlist
    if not list_config.list_len or short:
        return

    # Load formating string for profile
    fmt_string = perun_config.lookup_key_recursively('format.status')
    fmt_tokens = perun_log.scan_formatting_string(
        fmt_string, {}, default_fmt_callback=lambda token: "%" + token + "%"
    )
    adjust_header_length(fmt_tokens, max_lengths, list_config)

    # Print header (2 is padding for id)
    print_status_profile_list_header(fmt_tokens, list_config, max_lengths)

    # Print profiles
    print_status_profiles(fmt_tokens, list_config, max_lengths, fmt_string, profiles)

def print_status_profiles(fmt_tokens, list_config, max_lengths, fmt_string, profiles):
    """Prints each of the profiles, formatted according to the formatting string

    The first profile, and every fifth profile is separated by horizontal line.

      0@p ┃ [time  ] ┃ time-example.perf                                     ┃
    ═════════════════════════════════════════════════════════════════════════▣
      1@p ┃ [mixed ] ┃ complexity-quicksort-[_]-[_]-2018-03-26-13-52-58.perf ┃
      2@p ┃ [memory] ┃ memory-mct-[_]-[_]-2018-03-26-12-16-36.perf           ┃
      3@p ┃ [mixed ] ┃ complexity-gif2bmp-[_]-[_]-2018-03-26-12-12-15.perf   ┃
      4@p ┃ [memory] ┃ memory-mct-[_]-[_]-2018-03-26-10-07-47.perf           ┃
      5@p ┃ [mixed ] ┃ complexity-quicksort-[_]-[_]-2018-03-22-17-04-52.perf ┃
    ═════════════════════════════════════════════════════════════════════════▣

    :param list fmt_tokens: list of pairs of (token type, token)
    :param ProfileInfoConfig list_config: configuration of the output profile list
    :param dict max_lengths: mapping of token types ot their maximal lengths for alignment
    :param str fmt_string: formatting string for error handling
    :param list profiles: list of profiles
    """
    for profile_no, profile_info in enumerate(profiles):
        perun_log.info(" ", end='')
        cprint("{}@{}".format(profile_no, list_config.id_char).rjust(list_config.id_width + 2, ' '),
               list_config.colour)
        perun_log.info(" ", end='')
        for (token_type, token) in fmt_tokens:
            if token_type == 'fmt_string':
                attr_type, limit, fill = FMT_REGEX.match(token).groups()
                limit = adjust_limit(limit, attr_type, max_lengths)
                print_other_formatting_string(
                    fmt_string, profile_info, attr_type, limit,
                    colour=list_config.colour, value_fill=fill or ' '
                )
            else:
                cprint(token, list_config.colour)
        perun_log.info("")
        if profile_no % 5 == 0 or profile_no == list_config.list_len - 1:
            cprintln("\u2550" * list_config.header_width + "\u25A3", list_config.colour)

def print_status_profile_list_header(fmt_tokens, list_config, max_lengths):
    """Prints the header of the profile list, printing each token aligned by maximal lengths.

    The example of header is as follows:

    ═════════════════════════════════════════════════════════════════════════▣
      id  ┃   type   ┃                         source                        ┃
    ═════════════════════════════════════════════════════════════════════════▣

    :param list fmt_tokens: list of pairs of (token type, token)
    :param ProfileInfoConfig list_config: configuration of the output profile list
    :param dict max_lengths: mapping of token types ot their maximal lengths for alignment
    """
    cprintln("\u2550" * list_config.header_width + "\u25A3", list_config.colour)
    perun_log.info(" ", end='')
    cprint("id".center(list_config.id_width + 2, ' '), list_config.colour)
    perun_log.info(" ", end='')
    for (token_type, token) in fmt_tokens:
        if token_type == 'fmt_string':
            attr_type, limit, _ = FMT_REGEX.match(token).groups()
            limit = adjust_limit(limit, attr_type, max_lengths, (2 if attr_type == 'type' else 0))
            token_string = attr_type.center(limit, ' ')
            cprint(token_string, list_config.colour)
        else:
            # Print the rest (non token stuff)
            cprint(token, list_config.colour)
    perun_log.info("")
    cprintln("\u2550" * list_config.header_width + "\u25A3", list_config.colour)

def adjust_header_length(fmt_tokens, max_lengths, list_config):
    """Ajdust the length of the header stored in configuration

    :param list fmt_tokens: list of tokens
    :param dict max_lengths: maximal lengths of individual tokens
    :param ProfileListConfig list_config: configuration of the printed list
    """
    # the magic constant three is for 3 border columns
    for (token_type, token) in fmt_tokens:
        if token_type == 'fmt_string':
            attr_type, limit, _ = FMT_REGEX.match(token).groups()
            limit = adjust_limit(limit, attr_type, max_lengths, (2 if attr_type == 'type' else 0))
            list_config.header_width += limit
        else:
            list_config.header_width += len(token)

def get_untracked_profiles():
    """Returns list untracked profiles, currently residing in the .perun/jobs directory.

    :returns list: list of ProfileInfo parsed from .perun/jobs directory
    """
    saved_entries = []
    profile_list = []
    # First load untracked files from the ./jobs/ directory
    untracked_list = sorted(
        list(filter(lambda f: f.endswith('perf'), os.listdir(pcs.get_job_directory())))
    )

    # Second load registered files in job index
    job_index = pcs.get_job_index()
    index.touch_index(job_index)
    with open(job_index, 'rb+') as index_handle:
        pending_index_entries = list(index.walk_index(index_handle))

    # Iterate through the index and check if it is still in the ./jobs directory
    # In case it is still valid, we extract it into ProfileInfo and remove it from the list
    #   of files in ./jobs directory
    for index_entry in pending_index_entries:
        if index_entry.path in untracked_list:
            real_path = os.path.join(pcs.get_job_directory(), index_entry.path)
            index_info = {
                'header': {
                    'type': index_entry.type,
                    'cmd': index_entry.cmd,
                    'args': index_entry.args,
                    'workload': index_entry.workload
                },
                'collector_info': {'name': index_entry.collector},
                'postprocessors': [
                    {'name': p} for p in index_entry.postprocessors
                    ]
            }
            profile_info = profile.ProfileInfo(
                index_entry.path, real_path, index_entry.time, index_info, is_raw_profile=True
            )
            profile_list.append(profile_info)
            saved_entries.append(index_entry)
            untracked_list.remove(index_entry.path)

    # Now for every non-registered file in the ./jobs/ directory, we load the profile,
    #   extract the info and register it in the index
    for untracked_path in untracked_list:
        real_path = os.path.join(pcs.get_job_directory(), untracked_path)
        time = timestamp.timestamp_to_str(os.stat(real_path).st_mtime)

        # Load the data from JSON, which contains additional information about profile
        loaded_profile = store.load_profile_from_file(real_path, is_raw_profile=True)
        registered_checksum = store.compute_checksum(real_path.encode('utf-8'))

        # Update the list of profiles and counters of types
        profile_info = profile.ProfileInfo(
            untracked_path, real_path, time, loaded_profile, is_raw_profile=True
        )
        untracked_entry = index.INDEX_ENTRY_CONSTRUCTORS[index.INDEX_VERSION - 1](
            time, registered_checksum, untracked_path, -1, loaded_profile
        )

        profile_list.append(profile_info)
        saved_entries.append(untracked_entry)

    # We write all of the entries that are valid in the ./jobs/ directory in the index
    index.write_list_of_entries(job_index, saved_entries)

    return profile_list

@perun_log.paged_function(paging_switch=turn_off_paging_wrt_config('status'))
def status(short=False, **_):
    """Prints the status of performance control system

    :param bool short: true if the output should be short (i.e. without some information)
    """
    # Obtain both of the heads
    major_head = vcs.get_head_major_version()
    minor_head = vcs.get_minor_head()

    # Print the status of major head.
    perun_log.info("On major version {} ".format(
        perun_log.in_color(major_head, TEXT_EMPH_COLOUR, TEXT_ATTRS)
    ), end='')

    # Print the index of the current head
    perun_log.info("(minor version: {})".format(
        perun_log.in_color(minor_head, TEXT_EMPH_COLOUR, TEXT_ATTRS)
    ))

    # Print in long format, the additional information about head commit, by default print
    if not short:
        perun_log.info("")
        minor_version = vcs.get_minor_version_info(minor_head)
        print_minor_version_info(minor_version)

    # Print profiles
    minor_version_profiles = profile.load_list_for_minor_version(minor_head)
    untracked_profiles = get_untracked_profiles()
    maxs = calculate_maximal_lengths_for_object_list(
        minor_version_profiles + untracked_profiles, profile.ProfileInfo.valid_attributes
    )
    print_status_profile_list(minor_version_profiles, maxs, short)
    if not short:
        perun_log.info("")
    print_status_profile_list(untracked_profiles, maxs, short, 'untracked')

    # Print degradation info
    degradation_list = store.load_degradation_list_for(
        pcs.get_object_directory(), minor_head
    )
    if not short:
        perun_log.info("")
    perun_log.print_short_summary_of_degradations(degradation_list)
    if not short:
        perun_log.info("")
        perun_log.print_list_of_degradations(degradation_list)

@vcs.lookup_minor_version
def load_profile_from_args(profile_name, minor_version):
    """
    :param Profile profile_name: profile that will be stored for the minor version
    :param str minor_version: SHA-1 representation of the minor version
    :returns dict: loaded profile represented as dictionary
    """
    profiled_looked_up_already = store.is_sha1(profile_name)
    profiles = [profile_name] if profiled_looked_up_already else []
    # If the profile is defined by its path, we have to first look up it in the index
    if not profiled_looked_up_already:
        _, minor_index_file = store.split_object_name(pcs.get_object_directory(), minor_version)
        # If there is nothing at all in the index, since it is not even created ;)
        #   we returning nothing otherwise we lookup entries in index
        if os.path.exists(minor_index_file):
            with open(minor_index_file, 'rb') as minor_handle:
                lookup_pred = lambda entry: entry.path == profile_name
                profiles.extend(index.lookup_all_entries_within_index(minor_handle, lookup_pred))

    # If there are more profiles we should chose
    if not profiles:
        return None
    chosen_profile = profiles[0]

    # Peek the type if the profile is correct and load the json
    _, profile_name = store.split_object_name(pcs.get_object_directory(), chosen_profile.checksum)
    loaded_profile = store.load_profile_from_file(profile_name, False)

    return loaded_profile

def print_temp_files(root, **kwargs):
    """Print the temporary files in the root directory.

    :param str root: the path to the directory that should be listed
    :param kwargs: additional parameters such as sorting, output formatting etc.
    """
    # Try to load the files in the root directory
    try:
        temp.synchronize_index()
        tmp_files = temp.list_all_temps_with_details(root)
    except InvalidTempPathException as exc:
        perun_log.error(str(exc))

    # Filter the files by protection level if it is set to show only certain group
    if kwargs['filter_protection'] != 'all':
        tmp_files = [(name, level, size) for name, level, size in tmp_files
                     if level == kwargs['filter_protection']]
    # If there are no files then abort the output
    if not tmp_files:
        perun_log.info('== No results for the given parameters in the .perun/tmp/ directory ==')
        return

    # First sort by the name
    tmp_files.sort(key=itemgetter(0))
    # Now apply 'sort-by' if it differs from name:
    if kwargs['sort_by'] != 'name':
        sort_map = temp.SORT_ATTR_MAP[kwargs['sort_by']]
        tmp_files.sort(key=itemgetter(sort_map['pos']), reverse=sort_map['reverse'])

    # Print the total files size if needed
    _print_total_size(sum(size for _, _, size in tmp_files), not kwargs['no_total_size'])
    # Print the file records
    print_formatted_temp_files(
        tmp_files, not kwargs['no_file_size'], not kwargs['no_protection_level']
    )

def print_formatted_temp_files(records, show_size, show_protection):
    """Format and print temporary file records as:
    size | protection level | path from tmp/ directory

    :param list records: the list of temporary file records as tuple (size, protection, path)
    :param bool show_size: flag indicating whether size for each file should be shown
    :param bool show_protection: if set to True, show the protection level of each file
    """
    # Absolute path might be a bit too long, we remove the path component to the tmp/ directory
    prefix = len(pcs.get_tmp_directory()) + 1
    for file_name, protection, size in records:
        # Print the size of each file
        if show_size:
            perun_log.info('{}'.format(
                perun_log.in_color(utils.format_file_size(size), TEXT_EMPH_COLOUR)
            ), end=perun_log.in_color(' | ', TEXT_WARN_COLOUR))
        # Print the protection level of each file
        if show_protection:
            if protection == temp.UNPROTECTED:
                perun_log.info('{}'.format(temp.UNPROTECTED),
                      end=perun_log.in_color(' | ', TEXT_WARN_COLOUR))
            else:
                perun_log.info('{}  '.format(perun_log.in_color(temp.PROTECTED, TEXT_WARN_COLOUR)),
                      end=perun_log.in_color(' | ', TEXT_WARN_COLOUR))

        # Print the file path, emphasize the directory to make it a bit more readable
        file_name = file_name[prefix:]
        file_dir = os.path.dirname(file_name)
        if file_dir:
            file_dir += os.sep
            perun_log.info('{}'.format(perun_log.in_color(file_dir, TEXT_EMPH_COLOUR)), end='')
        perun_log.info('{}'.format(os.path.basename(file_name)))

def delete_temps(path, ignore_protected, force, **kwargs):
    """Delete the temporary file(s) identified by the path. The path can be either file (= delete
    only the file) or directory (= delete files in the directory or the whole directory).

    :param str path: the path to the target file or directory
    :param bool ignore_protected: if True, protected files are ignored and not deleted, otherwise
                                  deletion process is aborted and exception is raised
    :param bool force: if True, delete also protected files
    :param kwargs: additional parameters
    """
    try:
        # Determine if path is file or directory and call the correct functions for that
        if temp.exists_temp_file(path):
            temp.delete_temp_file(path, ignore_protected, force)
        elif temp.exists_temp_dir(path):
            # We might delete only files or files + empty directories
            if kwargs['keep_directories']:
                temp.delete_all_temps(path, ignore_protected, force)
            else:
                temp.delete_temp_dir(path, ignore_protected, force)
        # The supplied path does not exist, inform the user so they can correct the path
        else:
            perun_log.warn("The supplied path '{}' does not exist, no files deleted"
                           .format(temp.temp_path(path)))
    except (InvalidTempPathException, ProtectedTempException) as exc:
        # Invalid path or protected files encountered
        perun_log.error(str(exc))

def list_stat_objects(mode, **kwargs):
    """ Prints the stat files or versions (based on the mode) in the '.perun/stats' directory.

    The default output formats are:
    'file size | minor version | file name' for files
    'directory size | minor version | files count' for versions
    However, the parameters in 'kwargs' can alter the format (hide certain properties etc.)

    :param str mode: the requested list mode: 'versions' or 'files'
    :param kwargs: additional parameters from the CLI such as coloring the output, sorting etc.
    """
    versions = stats.list_stat_versions(kwargs['from_minor'], kwargs['top'])
    versions = [(version, stats.list_stats_for_minor(version)) for version, _ in versions]

    # Abort the whole output if we have no versions
    if not versions:
        perun_log.info('== No results for the given parameters in the .perun/stats/ directory ==')
        return

    if mode == 'versions':
        # We need to print the versions, aggregate the files and their sizes
        results = [(sum(size for _, size in files), version, len(files))
                   for version, files in versions]
        properties = [
            (not kwargs['no_dir_size'], True),
            (True, False),
            (not kwargs['no_file_count'], False)
        ]
    else:
        # We need to print the files, create separate record for each file
        results = []
        for version, files in versions:
            # A bit more complicated since we also need records for empty version directories
            if files:
                results.extend([(size, version, file) for file, size in files])
            else:
                results.append((None, version, '-= No stats file =-'))
        # results = [(size, version, file) for version, files in versions for file, size in files]
        properties = [
            (not kwargs['no_file_size'], True),
            (not kwargs['no_minor'], False),
            (True, False)
        ]

    # Separate the results with no files since they cannot be properly sorted but still need
    # to be printed
    record_size = itemgetter(0)
    valid_results, empty_results = utils.partition_list(
        results, lambda item: record_size(item) is not None
    )

    # Print the total size if needed
    _print_total_size(
        sum(record_size(record) for record in valid_results), not kwargs['no_total_size']
    )
    # Sort by size if needed
    if kwargs['sort_by_size']:
        if mode == 'versions':
            results.sort(key=record_size, reverse=True)
        else:
            valid_results.sort(key=record_size, reverse=True)
            results = valid_results + empty_results

    # Format the size so that is's suitable for output
    results = [(utils.format_file_size(size), version, file) for size, version, file in results]
    # Print all the results
    _print_stat_objects(results, properties)

def _print_total_size(total_size, enabled):
    """ Prints the formatted total size of all displayed results.

    :param int total_size: the total size in bytes
    :param bool enabled: a flag describing if the total size should be displayed at all
    """
    if enabled:
        total_size = utils.format_file_size(total_size)
        perun_log.info('Total size of all the displayed files / directories: {}'.format(
            perun_log.in_color(total_size, TEXT_EMPH_COLOUR))
        )

def _print_stat_objects(stats_objects, properties):
    """ Prints stats objects (files, versions, other iterable etc.) in a general way.

    The stats object should be a list of items to print, where each item consists of some
    properties that may or may not be printed / colored, as set by the 'properties'.

    The 'properties' should be a list of tuples (print, colored) that define if the corresponding
    property (item property on a matching position) should be printed and colored.

    :param list stats_objects: list of stat objects to print
    :param list properties: list of settings for item properties / parts
    """
    # Iterate all the stats objects and parts of each object
    for item in stats_objects:
        record = ''
        for pos, prop in enumerate(item):
            # Check if we should print the property and if it should be colored
            show_property, colored = properties[pos]
            if show_property:
                # Add the delimiter if the record already has some properties to print
                if record:
                    record += perun_log.in_color(' | ', TEXT_WARN_COLOUR)
                record += perun_log.in_color(str(prop), TEXT_EMPH_COLOUR) if colored else str(prop)
        perun_log.info(record)

def delete_stats_file(name, in_minor, keep_directory):
    """ Deletes stats file in either a specific minor version or across all the versions in the
    stats directory.

    :param str name: the file name
    :param str in_minor: the minor version identification or '.' for global deletion
    :param bool keep_directory: possibly empty version directory after the deletion will be kept
                                in the stats directory if set to True.
    """
    if in_minor == '.':
        stats.delete_stats_file_across_versions(name, keep_directory)
    else:
        stats.delete_stats_file(name, in_minor, keep_directory)

def delete_stats_minor(minor, keep_directory):
    """ Deletes the minor version directory in the stats directory.

    :param str minor: the minor version identification
    :param bool keep_directory: the empty version directory will be kept
                                in the stats directory if True
    """
    stats.delete_version_dirs([minor], False, keep_directory)

def delete_stats_all(keep_directory):
    """ Deletes all items in the stats directory.

    :param bool keep_directory: the empty version directories will be kept
                                in the stats directory if True
    """
    stats.reset_stats(keep_directory)

def clean_stats(keep_custom, keep_empty):
    """ Cleans the stats directory, that is:
    - synchronizes the internal state of the stats directory, i.e. the index file
    - attempts to delete all distinguishable custom files and directories (some manually created or
      custom objects may not be identified if they have the correct format, e.g. version directory
      that was created manually but has a valid version counterpart in the VCS, manually created
      files in the version directory etc.)
    - deletes all empty version directories in the stats directory

    :param bool keep_custom: the custom objects are kept in the stats directory if set to True
    :param bool keep_empty: the empty version directories are not deleted if set to True
    """
    stats.clean_stats(keep_custom, keep_empty)

def sync_stats():
    """ Synchronize the stats directory contents with the index file - delete minor version records
    for deleted versions and add missing records for existing versions.
    """
    stats.synchronize_index()

def sync_temps():
    """Synchronizes the internal state of the index file so that it corresponds to some possible
    manual changes in the directory by the user.
    """
    temp.synchronize_index()


#Importamos la libreia de sqlite3
import sqlite3
from sqlite3 import Error

def Crear_Conexion():
    #Lo creamos en un try para eviar errores y el cierre del programa
    try:
        conn = sqlite3.connect('DB/Calendar.db')
        return conn
    except Error as e:
        print("Error de la conexion " + e)

#Metodo para insertar los datos
def Insertar_Fecha(data):
    #Creamos la conexio
    conn = Crear_Conexion()
    #Creamos el query
    sql =  """INSERT INTO CALENDAR (Fecha,Hora,Horario,Descripcion)
            VALUES(?,?,?,?)"""
    try:
        #Creamos un cursor para hacer las operaciones
        cur = conn.cursor()
        #Ejecutamos el query
        cur.execute(sql,data)
        conn.commit()
        #lastrowid genera el id automatico
        print("Fecha insertada Correctarmente")
        return True, cur.lastrowid
    except Error as e:
        print("Error al insertar " + e)
        return False
    #Cerramos las conexion
    finally:
        if conn:
            cur.close()
            conn.close()
#Metodo para actualizar Fecha
def Actualizar_Fecha(_id,data):
    #Creamos la conexio
    conn = Crear_Conexion()
    #Creamos el query
    sql =  f"""UPDATE CALENDAR SET Fecha = ?,Hora=?,Horario=?,Descripcion=?
            WHERE Id_Calendario = {_id}"""
    try:
        #Creamos un cursor para hacer las operaciones
        cur = conn.cursor()
        #Ejecutamos el query
        cur.execute(sql,data)
        conn.commit()
        #lastrowid genera el id automatico
        print("Fecha Modificada Correctarmente")
        return True
    except Error as e:
        print("Error al Modificar " + e)
        return False
    #Cerramos las conexion
    finally:
        if conn:
            cur.close()
            conn.close()
#Metodo para elimnar
def Eliminar_Evento(_id):
    conn = Crear_Conexion()
    sql = f"DELETE FROM CALENDAR WHERE Id_Calendario = {_id}"
    try:
        cur = conn.cursor()
        cur.execute(sql)
        conn.commit()
        print("Evento Eliminado")
        return True
    except Error as e:
        print("Error al elimar :" + str(e))
    finally:
        if conn:
            cur.close()
            conn.close()

#Sleccinamos todos los datos de la base de datos
def Seleccion_Fechas():
    #Creamos la conexio
    conn = Crear_Conexion()
    #Creamos el query
    sql = "SELECT * FROM Calendar"
    try:
        #Creamos un cursor para hacer las operaciones
        cur = conn.cursor()
        #Ejecutamos el query
        cur.execute(sql)
        calendar = cur.fetchall()
        return calendar
    except Error as e:
        print("Error al seleccinar " + e)
    #Cerramos las conexion
    finally:
        if conn:
            cur.close()
            conn.close()

def Seleccionar_Id(_id):
    conn = Crear_Conexion()
    sql = f"SELECT * FROM Calendar WHERE Id_Calendario = {_id}"

    try:
        cur = conn.cursor()
        cur.execute(sql)
        fecha = cur.fetchone()
        print("Datos seleccionados")
        return fecha
    except Error as e:
        print("Error al seleccionar " + str(e))
    finally:
        if conn:
            cur.close()
            conn.close()


'''
Author Name: Yasmine El Shafei
Get Even elements using filter
'''
size = int(input('Enter size: '))
numbers = []
for i in range(int(size)):
    element = int(input("Enter Element: "))
    numbers.append(element)
evenNumber = filter(lambda num : num % 2 == 0, numbers)
print(list(evenNumber))


import logging
from utils.errors import ApiException
from fastapi import FastAPI,Request
from fastapi.responses import JSONResponse, PlainTextResponse
from fastapi.exceptions import RequestValidationError
from starlette.middleware.cors import CORSMiddleware
from routes.route import api_router

from services.logs.set_logs import set_logger
# app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

#ログファイルを作成
_logger = logging.getLogger(__name__)
set_logger(_logger)

app = FastAPI()

# CORSを回避するために設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.exception_handler(ApiException)
async def http_exception_handler(request: Request, exc: ApiException):
    """
    例外エラーハンドラー関数

    この関数がないと自作例外がfastAPI側でエラーとして扱われず500エラーとなる
    """
    return JSONResponse(
        status_code= exc.status_code,
        content={"status":exc.status,"detail":exc.detail}
    )

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    """
    FastAPIのHTTPリクエスト検証例外をオーバーライドして
    ログファイルに出力できるようにする

    以下の検証例外を400エラーとして返す
    ・422
    ・400
    ・その他

    """
    _logger.warning(f"request failed. status_code = 400 detail = {str(exc)}")
    return PlainTextResponse(str(exc), status_code=400)

app.include_router(api_router)


from django import forms

from inventory.models import Item, ItemCategory

class SelectizeFormMixin:
    # pass

    def get_value(self, name):
        field = self.fields.get(name)
        if field:
            return field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))

    def full_clean(self):
        if self.data:
            self.data = self.data.copy()
            for name, field in self.fields.items():
                if name in self.Meta.selectize_create:
                    value = self.get_value(name)
                    if value and value.startswith('_'):
                        kwargs = {'name': value.strip('_')}
                        obj, __ = field.queryset.model.objects.get_or_create(**kwargs)
                        self.data[name] = obj.id
        super().full_clean()

class InventoryForm(SelectizeFormMixin, forms.ModelForm):
    category = forms.ModelChoiceField(
        queryset=ItemCategory.objects.all(),
        widget=forms.Select(attrs={'data-add-new': 'true'})
    )

    class Meta:
        model = Item
        fields = ('category', 'product_id', 'name', 'company', 'price', 'stock')
        selectize_create = ['category']


import issues.domain.emails
from issues.domain.model import Issue, IssueReporter
from issues.domain.ports import UnitOfWorkManager, IssueViewBuilder
from issues.domain import emails

class ReportIssueHandler:

    def __init__(self, uowm: UnitOfWorkManager):
        self.uowm = uowm

    def handle(self, cmd):
        reporter = IssueReporter(cmd.reporter_name, cmd.reporter_email)
        issue = Issue(cmd.issue_id, reporter, cmd.problem_description)

        with self.uowm.start() as tx:
            tx.issues.add(issue)
            tx.commit()

class TriageIssueHandler:

    def __init__(self, uowm: UnitOfWorkManager):
        self.uowm = uowm

    def handle(self, cmd):
        with self.uowm.start() as tx:
            issue = tx.issues.get(cmd.issue_id)
            issue.triage(cmd.priority, cmd.category)
            tx.commit()

class PickIssueHandler:

    def __init__(self, uowm: UnitOfWorkManager):
        self.uowm = uowm

    def handle(self, cmd):
        with self.uowm.start() as tx:
            issue = tx.issues.get(cmd.issue_id)
            issue.assign(cmd.picked_by)
            tx.commit()

class AssignIssueHandler:

    def __init__(self, uowm: UnitOfWorkManager):
        self.uowm = uowm

    def handle(self, cmd):
        with self.uowm.start() as tx:
            issue = tx.issues.get(cmd.issue_id)
            issue.assign(cmd.assigned_to, cmd.assigned_by)
            tx.commit()

class IssueAssignedHandler:

    def __init__(self, view_builder: IssueViewBuilder,
                 sender: emails.EmailSender):
        self.sender = sender
        self.view_builder = view_builder

    def handle(self, evt):
        data = self.view_builder.fetch(evt.issue_id)
        data.update(**evt._asdict())
        request = emails.MailRequest(
            emails.IssueAssignedToMe,
            emails.default_from_addr,
            emails.EmailAddress(evt.assigned_to),
        )

        self.sender.send(request, data)


from analysis.common import *

from data.models.analysis_models import TagRelated
from data.models.sof_models import QuestionTags, Tag

def get_tag_id(tag, tag_index):
    return tag_index[tag]

def add_unique_key():
    Tag.raw('ALTER TABLE questiontags add CONSTRAINT unique_question_tag UNIQUE (question_id,tag)').execute()

def drop_unique_key():
    Tag.raw('ALTER TABLE questiontags DROP UNIQUE KEY unique_question_tag').execute()

def index_tags_ids(min_question_num, save=True, msg=None):
    # step1: index tags with it's id
    tag_index = {}
    index_tag = {}
    for tag in Tag.raw(
            'select tag.id, tag.name from questiontags left join tag on questiontags.tag = tag.name group by questiontags.tag having count(question_id) > (%s)',
            min_question_num).execute():
        tag_index[tag.name] = tag.id
    for tag in tag_index:
        index_tag[tag_index[tag]] = tag
    save_step_data(tag_index, step='1-1', save=save, msg=msg)
    save_step_data(index_tag, step='1-2', save=save, msg=msg)
    logger.info(f'get tag index finished, tags num: {len(tag_index.keys())}')
    return tag_index, index_tag

def index_tags_questions(tag_index, save=True, msg=None):
    # step2: index questions and tags to find questions of tag and tags of question
    tag_questions_index = {}
    question_tags_index = {}
    for question_tag in QuestionTags.raw(
                    'select * from questiontags where tag in ' + str(tuple(tag_index.keys()))).execute():
        tag = question_tag.tag
        question_id = question_tag.question_id
        if tag in tag_questions_index:
            tag_questions_index[tag].add(question_id)
        else:
            tag_questions_index[tag] = {question_id}
        if question_id in question_tags_index:
            question_tags_index[question_id].add(tag)
        else:
            question_tags_index[question_id] = {tag}
    for question in question_tags_index:
        question_tags_index[question] = list(question_tags_index[question])
    for tag in tag_questions_index:
        tag_questions_index[tag] = list(tag_questions_index[tag])
    save_step_data(tag_questions_index, step='2-1', save=save, msg=msg)
    save_step_data(question_tags_index, step='2-2', save=save, msg=msg)
    logger.info(f'index tag questions finished, questions: {len(question_tags_index.keys())}')
    return tag_questions_index, question_tags_index

def get_related_couples(question_tags_index, save=True, msg=None):
    # get all related couple
    if question_tags_index is None:
        question_tags_index = load_step_file('2-2')
    related_set = set()
    for question_id in question_tags_index:
        tags = question_tags_index[question_id]
        for i in range(len(tags)):
            for j in range(i + 1, len(tags)):
                if tags[i] > tags[j]:
                    related = (tags[j], tags[i])
                else:
                    related = (tags[i], tags[j])
                if related not in related_set:
                    related_set.add(related)
    save_step_data(related_set, step='3', transfer_item=lambda item: f'{item[0]} {item[1]}', save=save, msg=msg)
    logger.info(f'get all related couple finished, related: {len(related_set)}')
    return related_set

def calculate_related_weight(tag_questions_index, related_set, weight_func=None, save=True, msg=None):
    # calculate weight of related couples
    if tag_questions_index is None:
        tag_questions_index = load_step_file('2-1')
    if related_set is None:
        related_set = load_step_file('3')
    if not weight_func:
        weight_func = WeightFuncs.inter_divide_min
    related_weight = []
    for related in related_set:
        tag1_count = len(tag_questions_index[related[0]])
        tag2_count = len(tag_questions_index[related[1]])
        all = len(set(tag_questions_index[related[0]]) | set(tag_questions_index[related[1]]))
        weight = weight_func(tag1_count, tag2_count, all)
        related_weight.append((related[0], related[1], weight))
    save_step_data(related_weight, step='4', transfer_item=lambda item: f'{item[0]} {item[1]} {item[2]}',
                   save=save, msg=msg)
    logger.info(f'calculate related weight finished, related: {len(related_weight)}')
    return related_weight

# def related_weight_filter(related_weight, min_weight=0.1, save=True, msg=None):
#     # delete related with small weight
#     if related_weight is None:
#         related_weight = load_step_file(4)
#     related_weight_filted = list(filter(lambda related: related[2] > min_weight, related_weight))
#     save_step_data(related_weight_filted, step='5', transfer_item=lambda item: f'{item[0]} {item[1]} {item[2]}',
#                    save=save, msg=msg)
#     logger.info(f'related weight filter finished, related: {len(related_weight_filted)}')
#     return related_weight_filted

def save_related_to_bd(related_weight):
    TagRelated.truncate_table()
    TagRelated.insert_many_execute(
        ({'tag_from': tag1, 'tag_to': tag2, 'weight': weight} for (tag1, tag2, weight) in related_weight)
    ).execute()

def tag_related_pretreatment(min_question_num=100, msg=None, weight_func=None, step_file_dir=None, step=None, **kwargs):
    tag_index = None
    tag_questions_index = None
    question_tags_index = None
    related_couples = None
    related_weight = None
    related_weight_filted = None
    index_tag = None

    if step_file_dir is not None:
        config['step_file_dir'] = step_file_dir
    if step is not None:
        config['step'] = step if step > 0 else 1
    step = config['step']

    if config['step'] <= 1:
        (tag_index, index_tag) = index_tags_ids(min_question_num, msg=msg)
    if config['step'] <= 2:
        if tag_index is None:
            tag_index = load_step_file('1-1')
        (tag_questions_index, question_tags_index) = index_tags_questions(tag_index, msg=msg)
    if config['step'] <= 3:
        related_couples = get_related_couples(question_tags_index, msg=msg)
    if config['step'] <= 4:
        related_weight = calculate_related_weight(tag_questions_index, related_couples, msg=msg, weight_func=weight_func)
    if config['step'] <= 5:
        pass
        # save_related_to_bd(related_weight)

if __name__ == '__main__':
    min_question_num = 100
    weight_func = WeightFuncs.inter_divide_min
    msg = f'min_question_num={min_question_num}, weight_func={weight_func.__name__}'
    tag_related_pretreatment(min_question_num=min_question_num, weight_func=weight_func, msg=msg)
    # print(create_tag('xxxxxxx'))


import os

import torch
from torch import nn
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"

# 为了方便起见，我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重，并对输入和输出提高和缩减相应的维数
def comp_conv2d(conv2d, X):
    # 这里的（1，1）表示批量大小和通道数都是1
    X = X.reshape((1, 1) + X.shape)
    Y = conv2d(X)
    # 省略前两个维度：批量大小和通道
    return Y.reshape(Y.shape[2:])

# 请注意，这里每边都填充了1行或1列，因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
print(comp_conv2d(conv2d, X).shape)

# 如下示例中，我们使用高度为5，宽度为3的卷积核，高度和宽度两边的填充分别为2和1
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
print(comp_conv2d(conv2d, X).shape)

# 我们将高度和宽度的步幅设置为2，从而将输入的高度和宽度减半
# 在输入图像的边界填充元素称为填充（padding） 每次滑动元素的数量称为步幅（stride）
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
print(comp_conv2d(conv2d, X).shape)

conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
print(comp_conv2d(conv2d, X).shape)

# 在实践中，我们很少使用不一致的步幅或填充


import torch
import torch.nn as nn
import torch.nn.functional as F

from config import Config, load_pkl, pkl_parser
from env import Env_tsp

class Greedy(nn.Module):
	def __init__(self):
		super().__init__()

	def forward(self, log_p):
		return torch.argmax(log_p, dim = 1).long()

class Categorical(nn.Module):
	def __init__(self):
		super().__init__()

	def forward(self, log_p):
		return torch.multinomial(log_p.exp(), 1).long().squeeze(1)

# https://github.com/higgsfield/np-hard-deep-reinforcement-learning/blob/master/Neural%20Combinatorial%20Optimization.ipynb
class PtrNet1(nn.Module):
	def __init__(self, cfg):
		super().__init__()
		self.Embedding = nn.Linear(2, cfg.embed, bias = False)
		self.Encoder = nn.LSTM(input_size = cfg.embed, hidden_size = cfg.hidden, batch_first = True)
		self.Decoder = nn.LSTM(input_size = cfg.embed, hidden_size = cfg.hidden, batch_first = True)
		if torch.cuda.is_available():
			self.Vec = nn.Parameter(torch.cuda.FloatTensor(cfg.embed))
			self.Vec2 = nn.Parameter(torch.cuda.FloatTensor(cfg.embed))
		else:
			self.Vec = nn.Parameter(torch.FloatTensor(cfg.embed))
			self.Vec2 = nn.Parameter(torch.FloatTensor(cfg.embed))
		self.W_q = nn.Linear(cfg.hidden, cfg.hidden, bias = True)
		self.W_ref = nn.Conv1d(cfg.hidden, cfg.hidden, 1, 1)
		self.W_q2 = nn.Linear(cfg.hidden, cfg.hidden, bias = True)
		self.W_ref2 = nn.Conv1d(cfg.hidden, cfg.hidden, 1, 1)
		self.dec_input = nn.Parameter(torch.FloatTensor(cfg.embed))
		self._initialize_weights(cfg.init_min, cfg.init_max)
		self.clip_logits = cfg.clip_logits
		self.softmax_T = cfg.softmax_T
		self.n_glimpse = cfg.n_glimpse
		self.city_selecter = {'greedy': Greedy(), 'sampling': Categorical()}.get(cfg.decode_type, None)

	def _initialize_weights(self, init_min = -0.08, init_max = 0.08):
		for param in self.parameters():
			nn.init.uniform_(param.data, init_min, init_max)

	def forward(self, x, device):
		'''	x: (batch, city_t, 2)
			enc_h: (batch, city_t, embed)
			dec_input: (batch, 1, embed)
			h: (1, batch, embed)
			return: pi: (batch, city_t), ll: (batch)
		'''
		x = x.to(device)
		batch, city_t, _ = x.size()
		embed_enc_inputs = self.Embedding(x)
		embed = embed_enc_inputs.size(2)
		mask = torch.zeros((batch, city_t), device = device)
		enc_h, (h, c) = self.Encoder(embed_enc_inputs, None)
		ref = enc_h
		pi_list, log_ps = [], []
		dec_input = self.dec_input.unsqueeze(0).repeat(batch,1).unsqueeze(1).to(device)
		for i in range(city_t):
			_, (h, c) = self.Decoder(dec_input, (h, c))
			query = h.squeeze(0)
			for i in range(self.n_glimpse):
				query = self.glimpse(query, ref, mask)
			logits = self.pointer(query, ref, mask)
			log_p = torch.log_softmax(logits, dim = -1)
			next_node = self.city_selecter(log_p)
			dec_input = torch.gather(input = embed_enc_inputs, dim = 1, index = next_node.unsqueeze(-1).unsqueeze(-1).repeat(1, 1, embed))

			pi_list.append(next_node)
			log_ps.append(log_p)
			mask += torch.zeros((batch,city_t), device = device).scatter_(dim = 1, index = next_node.unsqueeze(1), value = 1)

		pi = torch.stack(pi_list, dim = 1)
		ll = self.get_log_likelihood(torch.stack(log_ps, 1), pi)
		return pi, ll

	def glimpse(self, query, ref, mask, inf = 1e8):
		"""	-ref about torch.bmm, torch.matmul and so on
			https://qiita.com/tand826/items/9e1b6a4de785097fe6a5
			https://qiita.com/shinochin/items/aa420e50d847453cc296

				Args:
			query: the hidden state of the decoder at the current
			(batch, 128)
			ref: the set of hidden states from the encoder.
			(batch, city_t, 128)
			mask: model only points at cities that have yet to be visited, so prevent them from being reselected
			(batch, city_t)
		"""
		u1 = self.W_q(query).unsqueeze(-1).repeat(1,1,ref.size(1))# u1: (batch, 128, city_t)
		u2 = self.W_ref(ref.permute(0,2,1))# u2: (batch, 128, city_t)
		V = self.Vec.unsqueeze(0).unsqueeze(0).repeat(ref.size(0), 1, 1)
		u = torch.bmm(V, torch.tanh(u1 + u2)).squeeze(1)
		# V: (batch, 1, 128) * u1+u2: (batch, 128, city_t) => u: (batch, 1, city_t) => (batch, city_t)
		u = u - inf * mask
		a = F.softmax(u / self.softmax_T, dim = 1)
		d = torch.bmm(u2, a.unsqueeze(2)).squeeze(2)
		# u2: (batch, 128, city_t) * a: (batch, city_t, 1) => d: (batch, 128)
		return d

	def pointer(self, query, ref, mask, inf = 1e8):
		"""	Args:
			query: the hidden state of the decoder at the current
			(batch, 128)
			ref: the set of hidden states from the encoder.
			(batch, city_t, 128)
			mask: model only points at cities that have yet to be visited, so prevent them from being reselected
			(batch, city_t)
		"""
		u1 = self.W_q2(query).unsqueeze(-1).repeat(1,1,ref.size(1))# u1: (batch, 128, city_t)
		u2 = self.W_ref2(ref.permute(0,2,1))# u2: (batch, 128, city_t)
		V = self.Vec2.unsqueeze(0).unsqueeze(0).repeat(ref.size(0), 1, 1)
		u = torch.bmm(V, self.clip_logits * torch.tanh(u1 + u2)).squeeze(1)
		# V: (batch, 1, 128) * u1+u2: (batch, 128, city_t) => u: (batch, 1, city_t) => (batch, city_t)
		u = u - inf * mask
		return u

	def get_log_likelihood(self, _log_p, pi):
		"""	args:
			_log_p: (batch, city_t, city_t)
			pi: (batch, city_t), predicted tour
			return: (batch)
		"""
		log_p = torch.gather(input = _log_p, dim = 2, index = pi[:,:,None])
		return torch.sum(log_p.squeeze(-1), 1)

if __name__ == '__main__':
	cfg = load_pkl(pkl_parser().path)
	model = PtrNet1(cfg)
	inputs = torch.randn(3,20,2)
	pi, ll = model(inputs, device = 'cpu')
	print('pi:', pi.size(), pi)
	print('log_likelihood:', ll.size(), ll)

	cnt = 0
	for i, k in model.state_dict().items():
		print(i, k.size(), torch.numel(k))
		cnt += torch.numel(k)
	print('total parameters:', cnt)

	# ll.mean().backward()
	# print(model.W_q.weight.grad)

	cfg.batch = 3
	env = Env_tsp(cfg)
	cost = env.stack_l(inputs, pi)
	print('cost:', cost.size(), cost)
	cost = env.stack_l_fast(inputs, pi)
	print('cost:', cost.size(), cost)


# 605. Can Place Flowers
# You have a long flowerbed in which some of the plots are planted, and some are not. However, flowers cannot be planted in adjacent plots.
# Given an integer array flowerbed containing 0's and 1's, where 0 means empty and 1 means not empty, and an integer n, return if n new flowers can be planted in the flowerbed without violating the no-adjacent-flowers rule.

from typing import List
class Solution:
    def canPlaceFlowers(self, flowerbed: List[int], n: int) -> bool:
        res = [0] + flowerbed + [0]
        for i in range(1, len(res) - 1):
            if res[i - 1] == 0 and res[i] == 0 \
                and res[i + 1] == 0:
                res[i] = 1
                n = n -1
        return True if n <= 0 else False

# 这道题的思路，
# 我们可以想象最左边和最右边各自有一个空格
# 我们再去遍历这个花床，如果它自己，它右边，它左边为空，则可以放置一盆花

flowerbed = [1,0,0,0,1]
n = 1
print(Solution().canPlaceFlowers(flowerbed, n))
flowerbed = [1,0,0,0,1]
n = 2
print(Solution().canPlaceFlowers(flowerbed, n))


import random
import time
import unittest
from pathlib import Path
from dataclasses import dataclass
import json

from weighted_union_find_tree import WeightedUnionFindTree

@dataclass(frozen=True)
class Testcase:
  n: int
  q: int
  queries: list[list[int]]
  want: list[int|None]

  @staticmethod
  def from_json(filepath:str) -> 'Testcase':
    with open(filepath) as f:
      obj = json.load(f)

    return Testcase(n = obj["n"], q = obj["q"], queries = obj["queries"], want = obj["want"])

class WeightedUnionTestCase(unittest.TestCase):
  def test_union(self):
    testcase_filepath = Path(__file__).parent / "weighted_union_testcase.json"
    testcase = Testcase.from_json(testcase_filepath)
    wuft = WeightedUnionFindTree(range(testcase.n))
    actual = self.__apply_queries(testcase.queries, wuft)
    self.assertListEqual(actual, testcase.want)

  def test_performance(self):
    data_size = 100000
    query_size = 200000
    uft = WeightedUnionFindTree(range(data_size))
    def gen_random_query(size: int):
      query_type = random.randint(0, 1)
      x = random.randint(0, size-2)
      y = random.randint(x+1, size-1)
      if query_type == 0:
        return [0, x, y, 0]
      else:
        return [1, x, y]

    queries = map(lambda x: gen_random_query(data_size), range(query_size))
    start_time = time.process_time()
    self.__apply_queries(queries, uft)
    end_time = time.process_time()
    #200000個のクエリーを2秒以内で完了させることを期待
    self.assertLess(end_time - start_time, 2)

  def __apply_queries(self, queries:list[list[int]], wuft: WeightedUnionFindTree) -> list[int|None]:
    actual: list[int|None] = []
    for query in queries:
      x = query[1]
      y = query[2]
      if query[0] == 0:
        z = query[3]
        wuft.union(x, y, z)
      else:
        delta = wuft.diff(y, x)
        actual.append(delta)

    return actual

if __name__ == "__main__":
  unittest.main()


from copy import deepcopy
from typing import Dict, List, Tuple

import torch
from torch import Tensor
from torch.nn import Module

weighted_modules = [
    'Conv1d', 'Conv2d', 'Conv3d', 'ConvTranspose1d', 'ConvTranspose2d', 'ConvTranspose3d',
    'Linear', 'Bilinear',
    'PReLU',
    'Embedding', 'EmbeddingBag',
]

def config_list_canonical(model: Module, config_list: List[Dict]) -> List[Dict]:
    '''
    Split the config by op_names if 'sparsity' or 'sparsity_per_layer' in config,
    and set the sub_config['total_sparsity'] = config['sparsity_per_layer'].
    And every item in 'op_partial_names' will match corresponding 'op_names' in model,
    then convert 'op_partial_names' to 'op_names' in config.

    Example::
        model = models.resnet18()
        config_list = [{'op_types': ['Conv2d'], 'sparsity': 0.8, 'op_partial_names': ['conv1']}]
        pruner = L1NormPruner(model, config_list)
        pruner.compress()
        pruner.show_pruned_weights()

    In this process, the config_list will implicitly convert to the following:

    [{'op_types': ['Conv2d'], 'sparsity_per_layer': 0.8,
        'op_names': ['conv1', 'layer1.0.conv1', 'layer1.1.conv1',
        'layer2.0.conv1', 'layer2.1.conv1', 'layer3.0.conv1', 'layer3.1.conv1',
        'layer4.0.conv1', 'layer4.1.conv1']}]
    '''
    for config in config_list:
        if 'sparsity' in config:
            if 'sparsity_per_layer' in config:
                raise ValueError("'sparsity' and 'sparsity_per_layer' have the same semantics, can not set both in one config.")
            else:
                config['sparsity_per_layer'] = config.pop('sparsity')

    for config in config_list:
        if 'op_types' in config:
            if 'default' in config['op_types']:
                config['op_types'].remove('default')
                config['op_types'].extend(weighted_modules)

    for config in config_list:
        if 'op_partial_names' in config:
            op_names = []
            for partial_name in config['op_partial_names']:
                for name, _ in model.named_modules():
                    if partial_name in name:
                        op_names.append(name)
            if 'op_names' in config:
                config['op_names'].extend(op_names)
                config['op_names'] = list(set(config['op_names']))
            else:
                config['op_names'] = op_names
            config.pop('op_partial_names')

    config_list = dedupe_config_list(unfold_config_list(model, config_list))
    new_config_list = []

    for config in config_list:
        if 'sparsity_per_layer' in config:
            sparsity_per_layer = config.pop('sparsity_per_layer')
            op_names = config.pop('op_names')
            for op_name in op_names:
                sub_config = deepcopy(config)
                sub_config['op_names'] = [op_name]
                sub_config['total_sparsity'] = sparsity_per_layer
                new_config_list.append(sub_config)
        elif 'max_sparsity_per_layer' in config and isinstance(config['max_sparsity_per_layer'], float):
            op_names = config.get('op_names', [])
            max_sparsity_per_layer = {}
            max_sparsity = config['max_sparsity_per_layer']
            for op_name in op_names:
                max_sparsity_per_layer[op_name] = max_sparsity
            config['max_sparsity_per_layer'] = max_sparsity_per_layer
            new_config_list.append(config)
        else:
            new_config_list.append(config)

    return new_config_list

def unfold_config_list(model: Module, config_list: List[Dict]) -> List[Dict]:
    '''
    Unfold config_list to op_names level.
    '''
    unfolded_config_list = []
    for config in config_list:
        op_names = []
        for module_name, module in model.named_modules():
            module_type = type(module).__name__
            if 'op_types' in config and module_type not in config['op_types']:
                continue
            if 'op_names' in config and module_name not in config['op_names']:
                continue
            op_names.append(module_name)
        unfolded_config = deepcopy(config)
        unfolded_config['op_names'] = op_names
        unfolded_config_list.append(unfolded_config)
    return unfolded_config_list

def dedupe_config_list(config_list: List[Dict]) -> List[Dict]:
    '''
    Dedupe the op_names in unfolded config_list.
    '''
    exclude = set()
    exclude_idxes = []
    config_list = deepcopy(config_list)
    for idx, config in reversed(list(enumerate(config_list))):
        if 'exclude' in config:
            exclude.update(config['op_names'])
            exclude_idxes.append(idx)
            continue
        config['op_names'] = sorted(list(set(config['op_names']).difference(exclude)))
        exclude.update(config['op_names'])
    for idx in sorted(exclude_idxes, reverse=True):
        config_list.pop(idx)
    return config_list

def compute_sparsity_compact2origin(origin_model: Module, compact_model: Module, config_list: List[Dict]) -> List[Dict]:
    """
    Compare origin model and compact model, return the sparsity of each group mentioned in config list.
    A group means all layer mentioned in one config.
    e.g., a linear named 'linear1' and its weight size is [100, 100] in origin model, but in compact model,
    the layer weight size with same layer name is [100, 50],
    then this function will return [{'op_names': 'linear1', 'total_sparsity': 0.5}].
    """
    compact2origin_sparsity = []
    for config in config_list:
        left_weight_num = 0
        total_weight_num = 0
        for module_name, module in origin_model.named_modules():
            module_type = type(module).__name__
            if 'op_types' in config and module_type not in config['op_types']:
                continue
            if 'op_names' in config and module_name not in config['op_names']:
                continue
            total_weight_num += module.weight.data.numel()
        for module_name, module in compact_model.named_modules():
            module_type = type(module).__name__
            if 'op_types' in config and module_type not in config['op_types']:
                continue
            if 'op_names' in config and module_name not in config['op_names']:
                continue
            left_weight_num += module.weight.data.numel()
        compact2origin_sparsity.append(deepcopy(config))
        compact2origin_sparsity[-1]['total_sparsity'] = 1 - left_weight_num / total_weight_num
    return compact2origin_sparsity

def compute_sparsity_mask2compact(compact_model: Module, compact_model_masks: Dict[str, Dict[str, Tensor]], config_list: List[Dict]):
    """
    Apply masks on compact model, return the sparsity of each group mentioned in config list.
    A group means all layer mentioned in one config.
    This function count all zero elements of the masks in one group,
    then divide by the elements number of the weights in this group to compute sparsity.
    """
    mask2compact_sparsity = []
    for config in config_list:
        left_weight_num = 0
        total_weight_num = 0
        for module_name, module in compact_model.named_modules():
            module_type = type(module).__name__
            if 'op_types' in config and module_type not in config['op_types']:
                continue
            if 'op_names' in config and module_name not in config['op_names']:
                continue
            module_weight_num = module.weight.data.numel()
            total_weight_num += module_weight_num
            if module_name in compact_model_masks:
                weight_mask = compact_model_masks[module_name]['weight']
                left_weight_num += len(torch.nonzero(weight_mask, as_tuple=False))
            else:
                left_weight_num += module_weight_num
        mask2compact_sparsity.append(deepcopy(config))
        mask2compact_sparsity[-1]['total_sparsity'] = 1 - left_weight_num / total_weight_num
    return mask2compact_sparsity

def compute_sparsity(origin_model: Module, compact_model: Module, compact_model_masks: Dict[str, Dict[str, Tensor]],
                     config_list: List[Dict]) -> Tuple[List[Dict], List[Dict], List[Dict]]:
    """
    This function computes how much the origin model has been compressed in the current state.
    The current state means `compact_model` + `compact_model_masks`
    (i.e., `compact_model_masks` applied on `compact_model`).
    The compact model is the origin model after pruning,
    and it may have different structure with origin_model cause of speed up.

    Returns
    -------
    Tuple[List[Dict], List[Dict], List[Dict]]
        (current2origin_sparsity, compact2origin_sparsity, mask2compact_sparsity).
        current2origin_sparsity is how much the origin model has been compressed in the current state.
        compact2origin_sparsity is the sparsity obtained by comparing the structure of origin model and compact model.
        mask2compact_sparsity is the sparsity computed by count the zero value in the mask.
    """
    compact2origin_sparsity = compute_sparsity_compact2origin(origin_model, compact_model, config_list)
    mask2compact_sparsity = compute_sparsity_mask2compact(compact_model, compact_model_masks, config_list)
    assert len(compact2origin_sparsity) == len(mask2compact_sparsity), 'Length mismatch.'
    current2origin_sparsity = []
    for c2o_sparsity, m2c_sparsity, config in zip(compact2origin_sparsity, mask2compact_sparsity, config_list):
        current2origin_sparsity.append(deepcopy(config))
        current2origin_sparsity[-1]['total_sparsity'] = 1 - (1 - c2o_sparsity['total_sparsity']) * (1 - m2c_sparsity['total_sparsity'])
    return current2origin_sparsity, compact2origin_sparsity, mask2compact_sparsity

def get_model_weights_numel(model: Module, config_list: List[Dict], masks: Dict[str, Dict[str, Tensor]] = {}) -> Dict:
    """
    Count the layer weight elements number in config_list.
    If masks is not empty, the masked weight will not be counted.
    """
    model_weights_numel = {}
    masked_rate = {}
    for config in config_list:
        for module_name, module in model.named_modules():
            module_type = type(module).__name__
            if 'op_types' in config and module_type not in config['op_types']:
                continue
            if 'op_names' in config and module_name not in config['op_names']:
                continue
            if module_name in masks and isinstance(masks[module_name]['weight'], Tensor):
                weight_mask = masks[module_name]['weight']
                masked_rate[module_name] = 1 - (weight_mask.sum().item() / weight_mask.numel())
                model_weights_numel[module_name] = round(weight_mask.sum().item())
            else:
                model_weights_numel[module_name] = module.weight.data.numel()
    return model_weights_numel, masked_rate

# FIXME: to avoid circular import, copy this function in this place
def get_module_by_name(model, module_name):
    """
    Get a module specified by its module name
    Parameters
    ----------
    model : pytorch model
        the pytorch model from which to get its module
    module_name : str
        the name of the required module
    Returns
    -------
    module, module
        the parent module of the required module, the required module
    """
    name_list = module_name.split(".")
    for name in name_list[:-1]:
        if hasattr(model, name):
            model = getattr(model, name)
        else:
            return None, None
    if hasattr(model, name_list[-1]):
        leaf_module = getattr(model, name_list[-1])
        return model, leaf_module
    else:
        return None, None


from django.test import TestCase

class SelectProjectTestCase(TestCase):
    def setUp(self):
        from bot.lib.maker import create_maker
        from bot.lib.project import create_project, update_project
        from bot.lib.material import create_material
        from bot.lib.pattern import create_pattern

        self.sender_id = '1'
        self.pattern_url = 'http://via.placeholder.com/350x150'
        self.pattern_type = 'image'
        self.material_url = 'http://via.placeholder.com/350x150'
        self.material_type = 'image'
        self.project_name = 'test_project'
        self.due_date = '2018-01-01'
        self.tags = ['1', '2', '3']
        self.update_url = 'http://via.placeholder.com/350x150'
        self.update_type = 'image'
        create_maker(sender_id=self.sender_id)
        project, created = create_project(sender_id=self.sender_id, name=self.project_name)
        self.project = project
        self.material = create_material(sender_id=self.sender_id, url=self.material_url, file_type=self.material_type)
        self.pattern = create_pattern(sender_id=self.sender_id, url=self.pattern_url, file_type=self.pattern_type)

        update_project(
            sender_id=self.sender_id,
            project_id=str(self.project.id),
            date_string=self.due_date,
            material_id=str(self.material.id),
            pattern_id=str(self.pattern.id),
            tags=self.tags
        )

    def test_select_project(self):
        from bot.conversations.update_project_status import project_selection

        response, new_context, conversation = project_selection.respond(
            sender_id=self.sender_id,
            message_text=None,
            attachment_type=None,
            attachment_url=None,
            postback=str(self.project.id),
            quick_reply=None,
            context=None)
        self.assertEqual(new_context["project_id"], str(self.project.id))

class ValidateProjectSelectionTestCase(TestCase):
    def setUp(self):
        from bot.lib.project import create_project
        from bot.lib.maker import create_maker
        maker, created = create_maker(sender_id='1')
        project, created = create_project(sender_id=maker.sender_id, name='Test_project')
        project_2, created = create_project(sender_id=maker.sender_id, name='Test_project_2')
        self.project = project

    def test_empty_message(self):
        from bot.conversations.update_project_status.project_selection import validate
        valid, message = validate(
            sender_id='1',
            message_text=None,
            attachment_type=None,
            postback=None,
            quick_reply=None
        )
        self.assertFalse(valid)

    def test_image(self):
        from bot.conversations.update_project_status.project_selection import validate
        valid, message = validate(
            sender_id='1',
            message_text=None,
            attachment_type='image',
            postback=None,
            quick_reply=None
        )
        self.assertFalse(valid)

    def test_valid_project(self):
        from bot.conversations.update_project_status.project_selection import validate
        valid, message = validate(
            sender_id='1',
            message_text=None,
            attachment_type=None,
            postback=str(self.project.id),
            quick_reply=None
        )
        self.assertTrue(valid)

    def test_valid_file(self):
        from bot.conversations.update_project_status.project_selection import validate
        valid, message = validate(
            sender_id='1',
            message_text='hello',
            attachment_type=None,
            postback=None,
            quick_reply=None
        )
        self.assertFalse(valid)


def calculateLps(pattern, lps):
    length = 0
    lps[0] = 0

    iterator = 1
    while iterator < len(pattern):
        if pattern[iterator] == pattern[length]:
            length += 1
            lps[iterator] = length
            iterator += 1
        else:
            if length != 0:
                length = lps[length - 1]
            else:
                lps[iterator] = 0
                iterator += 1

def KMPSearch(pattern, text):
    sizePattern = len(pattern)
    sizeText = len(text)

    # lps will hold the longest prefix suffix values for pattern
    lps = [0] * sizePattern
    j = 0

    calculateLps(pattern, lps)

    i = 0
    while i < sizeText:
        if pattern[j] == text[i]:
            i+=1
            j+=1

        if j == sizePattern:
            print("Pattern found at " + str(i - j + 1))
            j = lps[j - 1]
        elif i < sizeText and pattern[j] != text[i]:
            if j != 0:
                j = lps[j - 1]
            else:
                i += 1

#for dynamic input use the below code
# text =input()
# pattern = input()
# KMPSearch(pattern, text)

text = "aaanunaanuaunnanuubhanu"
pattern = "anu"
KMPSearch(pattern, text)

# ''' Output
# Pattern found at 3
# Pattern found at 8
# Pattern found at 15
# Pattern found at 21


import shelve
from person import Person
from manager import Mannager

bob = Person('Bob Smith', 44, 30000, 'software')
sue = Person('Sue Jones', 47, 40000, 'Hardware')
tom = Person('Tom Doe', age=50, pay=50000)

db = shelve.open(r'F:\Repository\sshtest\OOP\person_data\class-shelve')
db['bob'] = bob
db['sue'] = sue
db['tom'] = tom
db.close()