# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.

import os
import re
from shutil import rmtree, which

from .command import Command, default_bin


class CMake(Command):
    def __init__(self, cmake_bin=None):
        self.bin = default_bin(cmake_bin, "cmake")

    @staticmethod
    def default_generator():
        """ Infer default generator.

        Gives precedence to ninja if there exists an executable named `ninja`
        in the search path.
        """
        found_ninja = which("ninja")
        return "Ninja" if found_ninja else "Unix Makefiles"


cmake = CMake()


class CMakeDefinition:
    """ CMakeDefinition captures the cmake invocation arguments.

    It allows creating build directories with the same definition, e.g.
    ```
    build_1 = cmake_def.build("/tmp/build-1")
    build_2 = cmake_def.build("/tmp/build-2")

    ...

    build1.all()
    build2.all()
    """

    def __init__(self, source, build_type="release", generator=None,
                 definitions=None, env=None):
        """ Initialize a CMakeDefinition

        Parameters
        ----------
        source : str
                 Source directory where the top-level CMakeLists.txt is
                 located. This is usually the root of the project.
        generator : str, optional
        definitions: list(str), optional
        env : dict(str,str), optional
              Environment to use when invoking cmake. This can be required to
              work around cmake deficiencies, e.g. CC and CXX.
        """
        self.source = os.path.abspath(source)
        self.build_type = build_type
        self.generator = generator if generator else cmake.default_generator()
        self.definitions = definitions if definitions else []
        self.env = env

    @property
    def arguments(self):
        """" Return the arguments to cmake invocation. """
        arguments = [
            "-G{}".format(self.generator),
        ] + self.definitions + [
            self.source
        ]
        return arguments

    def build(self, build_dir, force=False, cmd_kwargs=None, **kwargs):
        """ Invoke cmake into a build directory.

        Parameters
        ----------
        build_dir : str
                    Directory in which the CMake build will be instantiated.
        force : bool
                If the build folder exists, delete it before. Otherwise if it's
                present, an error will be returned.
        """
        if os.path.exists(build_dir):
            # Extra safety to ensure we're deleting a build folder.
            if not CMakeBuild.is_build_dir(build_dir):
                raise FileExistsError(
                    "{} is not a cmake build".format(build_dir)
                )
            if not force:
                raise FileExistsError(
                    "{} exists use force=True".format(build_dir)
                )
            rmtree(build_dir)

        os.mkdir(build_dir)

        cmd_kwargs = cmd_kwargs if cmd_kwargs else {}
        cmake(*self.arguments, cwd=build_dir, env=self.env, **cmd_kwargs)
        return CMakeBuild(build_dir, self.build_type, definition=self,
                          **kwargs)

    def __repr__(self):
        return "CMakeDefinition[source={}]".format(self.source)


CMAKE_BUILD_TYPE_RE = re.compile("CMAKE_BUILD_TYPE:STRING=([a-zA-Z]+)")


class CMakeBuild(CMake):
    """ CMakeBuild represents a build directory initialized by cmake.

    The build instance can be used to build/test/install. It alleviates the
    user to know which generator is used.
    """

    def __init__(self, build_dir, build_type, definition=None):
        """ Initialize a CMakeBuild.

        The caller must ensure that cmake was invoked in the build directory.

        Parameters
        ----------
        definition : CMakeDefinition
                     The definition to build from.
        build_dir : str
                    The build directory to setup into.
        """
        assert CMakeBuild.is_build_dir(build_dir)
        super().__init__()
        self.build_dir = os.path.abspath(build_dir)
        self.build_type = build_type
        self.definition = definition

    @property
    def binaries_dir(self):
        return os.path.join(self.build_dir, self.build_type)

    def run(self, *argv, verbose=False, **kwargs):
        cmake_args = ["--build", self.build_dir, "--"]
        extra = []
        if verbose:
            extra.append("-v" if self.bin.endswith("ninja") else "VERBOSE=1")
        # Commands must be ran under the build directory
        return super().run(*cmake_args, *extra,
                           *argv, **kwargs, cwd=self.build_dir)

    def all(self):
        return self.run("all")

    def clean(self):
        return self.run("clean")

    def install(self):
        return self.run("install")

    def test(self):
        return self.run("test")

    @staticmethod
    def is_build_dir(path):
        """ Indicate if a path is CMake build directory.

        This method only checks for the existence of paths and does not do any
        validation whatsoever.
        """
        cmake_cache = os.path.join(path, "CMakeCache.txt")
        cmake_files = os.path.join(path, "CMakeFiles")
        return os.path.exists(cmake_cache) and os.path.exists(cmake_files)

    @staticmethod
    def from_path(path):
        """ Instantiate a CMakeBuild from a path.

        This is used to recover from an existing physical directory (created
        with or without CMakeBuild).

        Note that this method is not idempotent as the original definition will
        be lost. Only build_type is recovered.
        """
        if not CMakeBuild.is_build_dir(path):
            raise ValueError("Not a valid CMakeBuild path: {}".format(path))

        build_type = None
        # Infer build_type by looking at CMakeCache.txt and looking for a magic
        # definition
        cmake_cache_path = os.path.join(path, "CMakeCache.txt")
        with open(cmake_cache_path, "r") as cmake_cache:
            candidates = CMAKE_BUILD_TYPE_RE.findall(cmake_cache.read())
            build_type = candidates[0].lower() if candidates else "release"

        return CMakeBuild(path, build_type)

    def __repr__(self):
        return ("CMakeBuild["
                "build = {},"
                "build_type = {},"
                "definition = {}]".format(self.build_dir,
                                          self.build_type,
                                          self.definition))
