from ..core.helpers import itemize
from ..core.files import backendRep, expandDir, prefixSlash, normpath
from .helpers import splitModRef
from .repo import checkoutRepo
from .links import provenanceLink


# GET DATA FOR MAIN SOURCE AND ALL MODULES


class AppData:
    def __init__(
        self,
        app,
        backend,
        moduleRefs,
        bare,
        locations,
        modules,
        version,
        checkout,
        silent,
        dest=None,
        source=None,
    ):
        """Collects TF data according to specifications.

        The specifications are passed as arguments when the object is initialized.

        Parameters
        ----------
        backend: string
            `github` or `gitlab` or a GitLab instance such as `gitlab.huc.knaw.nl`.
        app: obj
            The high-level API object
        moduleRefs: tuple
            Each member consists of a module ref, which is a tuple of information
            that defines a module.
        bare: whether to load the standard modules
            If False, only modules explicitly given in moduleRefs are loaded.
            Standard modules will only be loaded if they are explicitly passed by
            moduleRefs.
            If True, standatd modules will always be loaded.
        locations: string|tuple
            One or more directory paths. They will be combined with the `modules`
            argument and used as locations to search for TF data files.
        modules: string|tuple
            One or more directory path segments. They will be appended to the
            paths given by the `locations` argument to form search locations
            for TF data files.
        version: string
            The version of TF data that should be retrieved. Version is a directory
            level just below the search locations.
        checkout: string
            A specifier to use a specific release or commit of a data repository.
        silent: string, optional tf.core.timestamp.SILENT_D
            See `tf.core.timestamp.Timestamp`
        dest: string, optional None
            The location where the text-fabric-data cache is.
        source: string, optional None
            The base of your local repository clones.

        """
        self.backend = backend
        self.app = app
        self.moduleRefs = (
            []
            if moduleRefs is None
            else moduleRefs.split(",")
            if type(moduleRefs) is str
            else list(moduleRefs)
        )
        self.bare = bare
        self.locationsArg = locations
        self.modulesArg = modules
        self.version = version
        self.checkout = checkout
        self.dest = dest
        self.source = source
        self.silent = silent

    def getMain(self):
        """Get the main data of the corpus.

        This is specified by the `org`, `repo` and `relative` settings under
        `provenanceSpec` in `config.yaml`.

        See Also
        --------
        tf.advanced.settings: options allowed in `config.yaml`
        """

        app = self.app
        source = self.source
        checkout = self.checkout
        aContext = app.context
        org = aContext.org
        repo = aContext.repo
        relative = prefixSlash(aContext.relative)
        appPath = aContext.appPath
        appName = aContext.appName

        if appName.startswith("app:"):
            appParent = appPath.rsplit("/", 1)[0]
            relative = f"{appParent}{relative}"
        elif org is None or repo is None:
            appPathRep = f"{appPath}/" if appPath else ""
            appParent = None
            relative = f"{appPathRep}{appName}"
            self.checkout = "local"
        else:
            appParent = None

        if not self.getModule(
            org,
            repo,
            prefixSlash(relative),
            checkout,
            isBase=True,
            appParent=appParent,
            versionOverride=app.versionOverride,
            allowExpress=True,
            source=source,
        ):
            self.good = False

    def getStandard(self):
        """Get the data of the standard modules specified by the settings of the corpus.

        These are specified in the `moduleSpecs` setting under
        `provenanceSpecs` in `config.yaml`.

        They will be loaded *after* the extra modules specified in the `mod`
        parameter, and only in as far they have not been specified in the
        `mod` parameter. In this way you can pass overriding
        checkout specifiers to the standard modules.

        See Also
        --------
        tf.advanced.settings: options allowed in `config.yaml`
        """

        bare = self.bare

        if not bare:
            return

        app = self.app
        source = self.source
        loadData = app.loadData

        if not loadData or loadData == "core":
            return

        aContext = app.context
        moduleSpecs = aContext.moduleSpecs
        seen = self.seen
        checkout = self.checkout
        backend = self.backend

        for m in moduleSpecs or []:
            org = m["org"]
            repo = m["repo"]
            relative = m["relative"]
            theCheckout = m.get("checkout", checkout)
            theBackend = m.get("backend", backend)
            bRep = backendRep(theBackend, "spec", default=backend)

            ref = f"{bRep}{org}/{repo}{relative}"
            if ref in seen:
                continue

            if not self.getModule(
                org,
                repo,
                relative,
                theCheckout,
                backend=theBackend,
                specs=m,
                versionOverride=app.versionOverride,
                allowExpress=True,
                source=source,
            ):
                self.good = False

    def getExtra(self):
        """Get the extra data specified by the settings of the corpus.

        These are specified in the `extraData` setting under
        `provenanceSpecs` in `config.yaml`.

        This data will be downloaded if needed, but it will not be loaded.
        It probably is not even TF data.

        See Also
        --------
        tf.advanced.settings: options allowed in `config.yaml`
        """

        bare = self.bare

        if not bare:
            return

        app = self.app
        source = self.source
        loadData = app.loadData

        if not loadData or loadData == "core":
            return

        aContext = app.context
        extraData = aContext.extraData

        if not extraData:
            return

        checkout = self.checkout
        backend = self.backend

        org = aContext.org
        repo = aContext.repo
        relative = extraData

        if not self.getModule(
            org,
            repo,
            relative,
            checkout,
            backend=backend,
            isExtra=True,
            versionOverride=app.versionOverride,
            allowExpress=True,
            source=source,
        ):
            self.good = False

    def getRefs(self):
        """Get data from additional modules.

        These are specified in the `moduleRefs` parameter of `AppData`.
        We store the set of special modules in order to skip them
        later when we are loading the standard modules.
        """

        app = self.app
        source = self.source
        backend = self.backend
        refs = self.moduleRefs

        for ref in refs:
            refPure = ref.rsplit(":", 1)[0]
            if refPure in self.seen:
                continue

            parts = splitModRef(ref)
            if not parts:
                self.good = False
                continue

            parts[2] = prefixSlash(normpath(parts[2]))  # the relative bit
            theBackend = (
                None if parts[-1] is None or parts[-1] == backend else parts[-1]
            )

            if not self.getModule(
                *parts[0:-1],
                backend=theBackend,
                versionOverride=app.versionOverride,
                allowExpress=False,
                source=source,
            ):
                self.good = False

    def getModules(self):
        """Get data from additional local directories.

        These are specified in the `locations` and `modules` parameters of `AppData`.
        """

        self.provenance = []
        provenance = self.provenance
        self.mLocations = []
        mLocations = self.mLocations

        self.locations = None
        self.modules = None

        self.good = True
        self.seen = set()

        self.getMain()
        self.getRefs()
        self.getStandard()
        self.getExtra()

        version = self.version
        good = self.good
        app = self.app

        if good:
            app.mLocations = mLocations
            app.provenance = provenance
        else:
            return

        mModules = []
        if mLocations:
            mModules.append(version or "")

        locations = self.locationsArg
        modules = self.modulesArg

        givenLocations = (
            []
            if locations is None
            else [expandDir(app, x.strip()) for x in itemize(locations, "\n")]
            if type(locations) is str
            else [str(x) for x in locations]
        )
        givenModules = (
            []
            if modules is None
            else [normpath(x.strip()) for x in itemize(modules, "\n")]
            if type(modules) is str
            else [normpath(str(x)) for x in modules]
        )

        self.locations = mLocations + givenLocations
        self.modules = mModules + givenModules

    def getModule(
        self,
        org,
        repo,
        relative,
        checkout,
        backend=None,
        isBase=False,
        appParent=None,
        isExtra=False,
        specs=None,
        versionOverride=False,
        allowExpress=True,
        source=None,
    ):
        """Prepare to load a single module.

        Eventually, all TF data will be downloaded from local directories, bases
        on a list of location paths and module paths.

        This function computes the contribution of a single module to both the
        location paths and the module paths.

        Parameters
        ----------
        org: string
            GitHub organization or GitLab group of the module
        repo: string:
            GitHub repository or GitLab project of the module
        relative: string
            Path within the repository of the module
        checkout: string
            A specifier to use a specific release or commit of a data repository.
        backend: string
            The back-end if different from the back-end of the main module
        isExtra: boolean, optional False
            If this is True, we are dealing with extra (non-TF) data.
            The data must be obtained, but not loaded.
        isBase: boolean, optional False
            Whether this module is the main data of the corpus.
        appParent: string, optional None
            Only relevant if `isBase` is `True`. Normally, the repoLocation attribute
            of the app will be set to the `relative` of the main module.
            But if the app has been specified with `app:`, the repoLocation is
            slightly different, and it will be taken from this parameter.
        specs: dict, optional False
            Additional informational attributes of the module, e.g. a DOI
        versionOverride: boolean, optional False
            Whether we ask for the data of a non-standard version
        allowExpress: boolean, optional True
            Whether express downloading of the whole corpus may be triggered
        source: string, optional None
            The base of your local repository clones.
        """

        backend = self.backend if backend is None else backendRep(backend, "norm")
        bRep = backendRep(backend, "spec", default=self.backend)
        dest = self.dest
        version = self.version
        silent = self.silent
        mLocations = self.mLocations
        provenance = self.provenance
        seen = self.seen
        app = self.app
        _browse = app._browse
        aContext = app.context
        branch = aContext.provenanceSpec["branch"]

        relative = prefixSlash(normpath(relative))

        moduleRef = f"{bRep}{org}/{repo}{relative}"

        if moduleRef in self.seen:
            return True

        if org is None or repo is None:
            relativeBare = relative if isBase else relative.removeprefix("/")
            repoLocation = relativeBare
            if not isExtra:
                mLocations.append(relativeBare)
            (commit, local, release) = (None, None, None)
        else:
            (commit, release, local, localBase, localDir) = checkoutRepo(
                backend,
                _browse=_browse,
                org=org,
                repo=repo,
                folder=relative,
                version=None if isExtra else version,
                versionOverride=versionOverride,
                checkout=checkout,
                dest=dest,
                withPaths=True if isExtra else False,
                keep=True if isExtra else False,
                silent=silent,
                allowExpress=allowExpress,
                source=source,
            )
            if not localBase:
                return False

            repoLocation = f"{localBase}/{org}/{repo}"
            if not isExtra:
                mLocations.append(f"{localBase}/{localDir}")

        seen.add(moduleRef)
        if isBase:
            app.repoLocation = repoLocation if appParent is None else appParent

        if isExtra:
            return True

        info = {}
        for item in (
            ("doi", None),
            ("corpus", f"{org}/{repo}{relative}"),
        ):
            (key, default) = item
            info[key] = (
                getattr(aContext, key)
                if isBase
                else specs[key]
                if specs and key in specs
                else default
            )
        provenance.append(
            (
                ("corpus", info["corpus"]),
                ("version", version),
                ("commit", commit or "??"),
                ("release", release or "none"),
                (
                    "live",
                    provenanceLink(
                        backend,
                        org,
                        repo,
                        version,
                        branch,
                        commit,
                        local,
                        release,
                        relative,
                    ),
                ),
                ("doi", info["doi"]),
            )
        )
        return True


def getModulesData(*args, dest=None, source=None):
    """Retrieve all data for a corpus.

    Parameters
    ----------
    args: list
        All parameters needed to retrieve all associated data.
        They are the same as are needed to construct an `AppData` object.
    """

    mData = AppData(*args, dest=dest, source=source)
    mData.getModules()

    if not mData.good or mData.locations is None:
        return None

    return (mData.locations, mData.modules)
