# Copyright 1998-2020 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

import logging
import operator
import sys
import signal

from _emerge.ProgressHandler import ProgressHandler

import portage
from portage import eapi_is_supported
from portage import os
from portage.cache.cache_errors import CacheError
from portage.eclass_cache import hashed_path
from portage.util import writemsg_level


def action_metadata(settings, portdb, myopts, porttrees=None):
    if porttrees is None:
        porttrees = portdb.porttrees
    portage.writemsg_stdout("\n>>> Updating Portage cache\n")
    cachedir = os.path.normpath(settings.depcachedir)
    if cachedir in [
        "/",
        "/bin",
        "/dev",
        "/etc",
        "/home",
        "/lib",
        "/opt",
        "/proc",
        "/root",
        "/sbin",
        "/sys",
        "/tmp",
        "/usr",
        "/var",
    ]:
        print(
            (
                "!!! PORTAGE_DEPCACHEDIR IS SET TO A PRIMARY "
                "ROOT DIRECTORY ON YOUR SYSTEM.\n"
                f"!!! This is ALMOST CERTAINLY NOT what you want: '{cachedir}'",
            ),
            file=sys.stderr,
        )
        sys.exit(73)
    if not os.path.exists(cachedir):
        os.makedirs(cachedir)

    auxdbkeys = portdb._known_keys

    class TreeData:
        __slots__ = ("dest_db", "eclass_db", "path", "src_db", "valid_nodes")

        def __init__(self, dest_db, eclass_db, path, src_db):
            self.dest_db = dest_db
            self.eclass_db = eclass_db
            self.path = path
            self.src_db = src_db
            self.valid_nodes = set()

    porttrees_data = []
    for path in porttrees:
        src_db = portdb._pregen_auxdb.get(path)
        if src_db is None:
            # portdbapi does not populate _pregen_auxdb
            # when FEATURES=metadata-transfer is enabled
            src_db = portdb._create_pregen_cache(path)

        if src_db is not None:
            eclass_db = portdb.repositories.get_repo_for_location(path).eclass_db
            # Update eclass data which may be stale after sync.
            eclass_db.update_eclasses()
            porttrees_data.append(TreeData(portdb.auxdb[path], eclass_db, path, src_db))

    quiet = (
        settings.get("TERM") == "dumb" or "--quiet" in myopts or not sys.stdout.isatty()
    )

    onProgress = None
    if not quiet:
        progressBar = portage.output.TermProgressBar()
        progressHandler = ProgressHandler()
        onProgress = progressHandler.onProgress

        def display():
            progressBar.set(progressHandler.curval, progressHandler.maxval)

        progressHandler.display = display

        def sigwinch_handler(signum, frame):
            lines, progressBar.term_columns = portage.output.get_term_size()

        signal.signal(signal.SIGWINCH, sigwinch_handler)

    # Temporarily override portdb.porttrees so portdb.cp_all()
    # will only return the relevant subset.
    portdb_porttrees = portdb.porttrees
    portdb.porttrees = (tree_data.path for tree_data in porttrees_data)
    try:
        cp_all = portdb.cp_all()
    finally:
        portdb.porttrees = portdb_porttrees

    curval = 0
    maxval = len(cp_all)
    if onProgress is not None:
        onProgress(maxval, curval)

    # TODO: Display error messages, but do not interfere with the progress bar.
    # Here's how:
    #  1) erase the progress bar
    #  2) show the error message
    #  3) redraw the progress bar on a new line

    for cp in cp_all:
        for tree_data in porttrees_data:
            src_chf = tree_data.src_db.validation_chf
            dest_chf = tree_data.dest_db.validation_chf
            dest_chf_key = f"_{dest_chf}_"
            dest_chf_getter = operator.attrgetter(dest_chf)

            for cpv in portdb.cp_list(cp, mytree=tree_data.path):
                tree_data.valid_nodes.add(cpv)
                try:
                    src = tree_data.src_db[cpv]
                except (CacheError, KeyError):
                    continue

                ebuild_location = portdb.findname(cpv, mytree=tree_data.path)
                if ebuild_location is None:
                    continue
                ebuild_hash = hashed_path(ebuild_location)

                try:
                    if not tree_data.src_db.validate_entry(
                        src, ebuild_hash, tree_data.eclass_db
                    ):
                        continue
                except CacheError:
                    continue

                eapi = src.get("EAPI")
                if not eapi:
                    eapi = "0"
                eapi_supported = eapi_is_supported(eapi)
                if not eapi_supported:
                    continue

                dest = None
                try:
                    dest = tree_data.dest_db[cpv]
                except (KeyError, CacheError):
                    pass

                for d in (src, dest):
                    if d is not None and d.get("EAPI") in ("", "0"):
                        del d["EAPI"]

                if src_chf != "mtime":
                    # src may contain an irrelevant _mtime_ which corresponds
                    # to the time that the cache entry was written
                    src.pop("_mtime_", None)

                if src_chf != dest_chf:
                    # populate src entry with dest_chf_key
                    # (the validity of the dest_chf that we generate from the
                    # ebuild here relies on the fact that we already used
                    # validate_entry to validate the ebuild with src_chf)
                    src[dest_chf_key] = dest_chf_getter(ebuild_hash)

                if dest is not None:
                    if not (
                        dest.get(dest_chf_key) == src[dest_chf_key]
                        and tree_data.eclass_db.validate_and_rewrite_cache(
                            dest["_eclasses_"],
                            tree_data.dest_db.validation_chf,
                            tree_data.dest_db.store_eclass_paths,
                        )
                        is not None
                        and set(dest["_eclasses_"]) == set(src["_eclasses_"])
                    ):
                        dest = None
                    else:
                        # We don't want to skip the write unless we're really
                        # sure that the existing cache is identical, so don't
                        # trust _mtime_ and _eclasses_ alone.
                        cache_is_identical = (
                            True for k in auxdbkeys if dest.get(k, "") != src.get(k, "")
                        )
                        if any(cache_is_identical):
                            dest = None
                if dest is not None:
                    # The existing data is valid and identical,
                    # so there's no need to overwrite it.
                    continue

                try:
                    tree_data.dest_db[cpv] = src
                except CacheError:
                    # ignore it; can't do anything about it.
                    pass

        curval += 1
        if onProgress is not None:
            onProgress(maxval, curval)

    if onProgress is not None:
        onProgress(maxval, curval)

    for tree_data in porttrees_data:
        try:
            dead_nodes = set(tree_data.dest_db)
        except CacheError as e:
            writemsg_level(
                (
                    "Error listing cache entries for "
                    f"'{tree_data.path}': {e}, continuing...\n"
                ),
                level=logging.ERROR,
                noiselevel=-1,
            )
            del e
        else:
            dead_nodes.difference_update(tree_data.valid_nodes)
            for cpv in dead_nodes:
                try:
                    del tree_data.dest_db[cpv]
                except (KeyError, CacheError):
                    pass

    if not quiet:
        # make sure the final progress is displayed
        progressHandler.display()
        print()
        signal.signal(signal.SIGWINCH, signal.SIG_DFL)

    portdb.flush_cache()
    sys.stdout.flush()
