#
#  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.
#
#  Authors:
#        Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
#        Tiago Gomes <tiago.gomes@codethink.co.uk>

"""
local - stage local files and directories
=========================================

**Usage:**

.. code:: yaml

   # Specify the local source kind
   kind: local

   # Specify the project relative path to a file or directory
   path: files/somefile.txt

See :ref:`built-in functionality doumentation <core_source_builtins>` for
details on common configuration options for sources.


Reporting :class:`.SourceInfo`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The local source reports the project relative path of the file or directory as the *url*.

Further, the local source reports the
:attr:`SourceInfoMedium.LOCAL <buildstream.source.SourceInfoMedium.LOCAL>` *medium* and the
:attr:`SourceVersionType.CAS_DIGEST <buildstream.source.SourceVersionType.CAS_DIGEST>` *version_type*,
for which it reports the CAS digest of the local source as the *version*.

The *guess_version* of a local source is meaningless, as it is tied instead to
the BuildStream project in which it is contained.
"""

import os
from buildstream import Source, SourceError, SourceInfoMedium, SourceVersionType, Directory


class LocalSource(Source):
    # pylint: disable=attribute-defined-outside-init

    BST_MIN_VERSION = "2.0"
    BST_STAGE_VIRTUAL_DIRECTORY = True

    __digest = None

    def configure(self, node):
        node.validate_keys(["path", *Source.COMMON_CONFIG_KEYS])
        self.path = self.node_get_project_path(node.get_scalar("path"))
        self.fullpath = os.path.join(self.get_project_directory(), self.path)

    def preflight(self):
        pass

    def is_resolved(self):
        return True

    def is_cached(self):
        return True

    def get_unique_key(self):
        #
        # As a core plugin, we use some private API to optimize file hashing.
        #
        # * Use Source._cache_directory() to prepare a Directory
        # * Do the regular staging activity into the Directory
        # * Use the hash of the cached digest as the unique key
        #
        self.__ensure_digest()
        return self.__digest.hash

    # We dont have a ref, we're a local file...
    def load_ref(self, node):
        pass

    def get_ref(self):
        return None  # pragma: nocover

    def set_ref(self, ref, node):
        pass  # pragma: nocover

    def fetch(self):  # pylint: disable=arguments-differ
        # Nothing to do here for a local source
        pass  # pragma: nocover

    def stage_directory(self, directory):
        #
        # We've already prepared the CAS while resolving the cache key which
        # will happen before staging.
        #
        # Now just retrieve the previously cached content to stage.
        #
        assert isinstance(directory, Directory)
        assert self.__digest is not None
        with self._cache_directory(digest=self.__digest) as cached_directory:
            directory.import_files(cached_directory, collect_result=False)

    def init_workspace_directory(self, directory):
        #
        # FIXME: We should be able to stage the workspace from the content
        #        cached in CAS instead of reimporting from the filesystem
        #        to the new workspace directory with this special case, but
        #        for some reason the writable bits are getting lost on regular
        #        files through the transition.
        #
        self.__do_stage(directory)

    def collect_source_info(self):
        self.__ensure_digest()
        version = "{}/{}".format(self.__digest.hash, self.__digest.size_bytes)
        return [self.create_source_info(self.path, SourceInfoMedium.LOCAL, SourceVersionType.CAS_DIGEST, version)]

    # As a core element, we speed up some scenarios when this is used for
    # a junction, by providing the local path to this content directly.
    #
    def _get_local_path(self):
        return self.fullpath

    # Ensure that the digest is resolved
    #
    def __ensure_digest(self):
        if not self.__digest:
            with self._cache_directory() as directory:
                self.__do_stage(directory)
                self.__digest = directory._get_digest()

    # Staging is implemented internally, we preemptively put it in the CAS
    # as a side effect of resolving the cache key, at stage time we just
    # do an internal CAS stage.
    #
    def __do_stage(self, directory):
        with self.timed_activity("Staging local files into CAS"):
            if os.path.isdir(self.fullpath) and not os.path.islink(self.fullpath):
                result = directory.import_files(self.fullpath)
            else:
                result = directory.import_single_file(self.fullpath)

            if result.overwritten or result.ignored:
                raise SourceError(
                    "Failed to stage source: files clash with existing directory", reason="ensure-stage-dir-fail"
                )


# Plugin entry point
def setup():
    return LocalSource
