# Copyright 2014 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""This modules implements the engine for cleaning packages in parallel"""

import pkg_resources
import sys
import time
import traceback
from queue import Queue


try:
    from catkin_pkg.packages import find_packages
    from catkin_pkg.topological_order import topological_order_packages
except ImportError as e:
    sys.exit(
        'ImportError: "from catkin_pkg.topological_order import '
        'topological_order" failed: %s\nMake sure that you have installed '
        '"catkin_pkg", and that it is up to date and on the PYTHONPATH.' % e
    )

from catkin_tools.execution.controllers import ConsoleStatusController
from catkin_tools.execution.executor import execute_jobs
from catkin_tools.execution.executor import run_until_complete

from catkin_tools.common import expand_glob_package
from catkin_tools.common import get_recursive_build_dependents_in_workspace
from catkin_tools.common import wide_log


def determine_packages_to_be_cleaned(context, include_dependents, packages):
    """Returns list of packages which should be cleaned, and those packages' deps.

    :param context: Workspace context
    :type context: :py:class:`catkin_tools.verbs.catkin_build.context.Context`
    :param include_dependents: Also clean dependents of the packages to be cleaned
    :type include_dependents: bool
    :param packages: list of package names to be cleaned
    :type packages: list
    :returns: full list of package names to be cleaned
    :rtype: list
    """

    # Get all the cached packages in the context source space
    workspace_packages = find_packages(context.package_metadata_path(), exclude_subspaces=True, warnings=[])
    # Order the packages by topology
    ordered_packages = topological_order_packages(workspace_packages)
    # Set the packages in the workspace for the context
    context.packages = ordered_packages

    # Create a dict of all packages in the workspace by name
    workspace_packages_by_name = dict([(pkg.name, (path, pkg)) for path, pkg in ordered_packages])

    # Initialize empty output
    packages_to_be_cleaned = set()

    # Expand glob patterns in packages
    expanded_packages = []
    for package_name in packages:
        expanded_packages.extend(expand_glob_package(package_name, workspace_packages_by_name))
    packages = expanded_packages

    # Expand metapackages into their constituents
    for package_name in packages:
        # This is ok if it's orphaned
        if package_name not in workspace_packages_by_name:
            packages_to_be_cleaned.add(package_name)
        else:
            # Get the package object
            package = workspace_packages_by_name[package_name][1]
            # If metapackage, include run depends which are in the workspace
            if 'metapackage' in [e.tagname for e in package.exports]:
                for rdep in package.run_depends:
                    if rdep.name in workspace_packages_by_name:
                        packages_to_be_cleaned.add(rdep.name)
            else:
                packages_to_be_cleaned.add(package_name)

    # Determine the packages that depend on the given packages
    if include_dependents:
        for package_name in list(packages_to_be_cleaned):
            # Get the packages that depend on the packages to be cleaned
            dependents = get_recursive_build_dependents_in_workspace(package_name, ordered_packages)
            packages_to_be_cleaned.update([pkg.name for _, pkg in dependents])

    return [workspace_packages_by_name[n] for n in packages_to_be_cleaned if n in workspace_packages_by_name]


def clean_packages(
        context,
        names_of_packages_to_be_cleaned,
        clean_dependents,
        verbose,
        dry_run):

    pre_start_time = time.time()

    # Update the names of packages to be cleaned with dependents
    packages_to_be_cleaned = determine_packages_to_be_cleaned(
        context,
        clean_dependents,
        names_of_packages_to_be_cleaned)

    # print(packages_to_be_cleaned)
    # for path, pkg in packages_to_be_cleaned:
    # if os.path.exists(os.path.join(context.build_space_abs, pkg.name)):
    # print("[clean] Cleaning package: %s" % pkg.name)

    # Construct jobs
    jobs = []
    for path, pkg in packages_to_be_cleaned:

        # Get all build type plugins
        clean_job_creators = {
            ep.name: ep.load()['create_clean_job']
            for ep in pkg_resources.iter_entry_points(group='catkin_tools.jobs')
        }

        # It's a problem if there aren't any build types available
        if len(clean_job_creators) == 0:
            sys.exit('Error: No build types available. Please check your catkin_tools installation.')

        # Determine the job parameters
        clean_job_kwargs = dict(
            context=context,
            package=pkg,
            package_path=path,
            dependencies=[],  # Unused because clean jobs are not parallelized
            dry_run=dry_run,
            clean_build=True,
            clean_devel=True,
            clean_install=True)

        # Create the job based on the build type
        build_type = pkg.get_build_type()

        if build_type in clean_job_creators:
            jobs.append(clean_job_creators[build_type](**clean_job_kwargs))

    if len(jobs) == 0:
        print("[clean] There are no products from the given packages to clean.")
        return False

    # Queue for communicating status
    event_queue = Queue()

    # Spin up status output thread
    status_thread = ConsoleStatusController(
        'clean',
        ['package', 'packages'],
        jobs,
        1,
        [pkg.name for _, pkg in context.packages],
        [p for p in context.buildlist],
        [p for p in context.skiplist],
        event_queue,
        show_notifications=False,
        show_active_status=False,
        show_buffered_stdout=verbose or False,
        show_buffered_stderr=True,
        show_live_stdout=False,
        show_live_stderr=False,
        show_stage_events=False,
        show_full_summary=False,
        pre_start_time=pre_start_time,
        active_status_rate=10.0)
    status_thread.start()

    # Initialize locks (none need to be configured here)
    locks = {
    }

    # Block while running N jobs asynchronously
    try:
        ej = execute_jobs(
            'clean',
            jobs,
            locks,
            event_queue,
            context.log_space_abs,
            max_toplevel_jobs=1,
            continue_on_failure=True,
            continue_without_deps=False)
        all_succeeded = run_until_complete(ej)
    except Exception:
        status_thread.keep_running = False
        all_succeeded = False
        status_thread.join(1.0)
        wide_log(str(traceback.format_exc()))

    status_thread.join(1.0)

    return all_succeeded
