#! /usr/bin/env python

import os
import re
import itertools
from abc import ABC, abstractmethod
from typing import List

from gen_config import BuildConfig
from find_pattern import print_color


def gen_constant_config() -> BuildConfig:
    """Generate shared constant build configurations for all tests."""
    config = BuildConfig()
    config.cmake_build_type = "Release"
    config.build_examples = True
    config.build_tests = False  # Disabled since it requires GoogleTest to be tested with -fPIC
    config.build_python_interface = True
    config.with_openmp = True
    return config


def run_command(cmd: str, file_name: str) -> List[str]:
    """
    Run command and get output from stdout and stderr.

    NOTE: we redirect stdout and stderr to file to avoid overflow.

    :param cmd: command to run
    :param file_name: file name of output to redirect stdout and stderr
    :return: output
    """
    os.system(f"{cmd} &> {file_name}")
    with open(file_name, "r") as output_file:
        cmake_output = output_file.readlines()
    return cmake_output


def search_pattern(content: List[str], pattern: str) -> bool:
    """
    Search for pattern in content.

    :param content: content in which the pattern is searched
    :param pattern: pattern to search
    :return: True if found, False otherwise.
    """
    matched = False
    for line in content:
        if re.search(pattern, line) is not None:
            matched = True
            break
    return matched


class BaseTest(ABC):
    """
    Abstract base class for all tests.

    Attributes
    ----------
    vendor: str
        vendor of compiler
    build_shared_libs: bool
        whether to build shared libraries (dll, so)
    flags: tuple
        miscellaneous shared flags among tests, vary among derived classes
    config: BuildConfig
        shared configurations among tests
    """
    def __init__(self):
        self.vendor = "gcc"
        self.build_shared_libs = True
        self.flags = None
        self.config = None

    def test_config(self, flags: tuple, message: str) -> None:
        """
        Test the 'configure' procedure.

        NOTE: This method also sets flags and config for test_build.

        :param flags: shared flags among tests
        :param message: error message
        """
        self.flags = flags
        self.config = self.gen_config()

        # Run cmake
        cmake_output = run_command(self.config.to_cmd().rstrip("\n"), "config.log")
        status = search_pattern(cmake_output, message)

        # Echo
        tag = f"{self.vendor} {self.build_shared_libs} {self.flags} config test"
        if status:
            print_color(f"{tag} passed", "g")
        else:
            print_color(f"{tag} FAILED", "r")

    def test_build(self) -> None:
        """Test the 'build' procedure"""
        targets = ["demo", "ffttest", "speedtest", "interface_diag",
                   "interface_mpi", "interface_tbpm"]
        if self.config.with_hdf5:
            targets.append("interface_hdf5")

        # Run cmake
        cmake_output = run_command("cmake --build . -j 8", "build.log")
        status = True
        for t in targets:
            status = (status and search_pattern(cmake_output, f"Built target {t}"))

        # Echo
        tag = f"{self.vendor} {self.build_shared_libs} {self.flags} build test"
        if status:
            print_color(f"{tag} passed", "g")
        else:
            print_color(f"{tag} FAILED", "r")

    @abstractmethod
    def gen_config(self) -> BuildConfig:
        """Generate configurations for testing."""
        pass

    @abstractmethod
    def run(self) -> None:
        """Actually tests should be placed here."""
        pass


class MathLibTest(BaseTest):
    def gen_config(self) -> BuildConfig:
        config = gen_constant_config()
        config.set_compiler_vendor(self.vendor)
        config.build_shared_libs = self.build_shared_libs
        config.with_mpi = False
        config.with_cuda = False
        config.with_hdf5 = False
        config.with_feast = self.flags[0]
        config.eigen_backend = self.flags[1]
        config.diag_backend = self.flags[2]
        config.tbpm_backend = self.flags[3]
        return config

    def run(self) -> None:
        vendors = ("gnu", "clang")
        build_shared = (True, False)
        for k in itertools.product(vendors, build_shared):
            self.vendor = k[0]
            self.build_shared_libs = k[1]

            # Bad paths
            self.test_config((False, "tmp", "default", "default"), "Unknown EIGEN_BACKEND")
            self.test_config((False, "default", "tmp", "default"), "Unknown DIAG_BACKEND")
            self.test_config((False, "default", "default", "tmp"), "Unknown TBPM_BACKEND")
            self.test_config((False, "aocl", "openblas", "default"), "Mixed usage")
            self.test_config((False, "aocl", "default", "mkl"), "TBPM_BACKEND must be default when EIGEN_BACKEND")
            self.test_config((False, "default", "aocl", "mkl"), "TBPM_BACKEND must be default when DIAG_BACKEND")
            self.test_config((True, "default", "default", "default"), "FEAST requires a working LAPACK library")

            # Happy paths
            self.test_config((False, "default", "default", "default"), "Generating done")
            self.test_build()
            #
            self.test_config((True, "aocl", "default", "default"), "Generating done")
            self.test_build()
            self.test_config((True, "openblas", "default", "default"), "Generating done")
            self.test_build()
            self.test_config((True, "netlib", "default", "default"), "Generating done")
            self.test_build()
            #
            self.test_config((True, "default", "aocl", "default"), "Generating done")
            self.test_build()
            self.test_config((True, "default", "openblas", "default"), "Generating done")
            self.test_build()
            self.test_config((True, "default", "netlib", "default"), "Generating done")
            self.test_build()
            #
            self.test_config((True, "aocl", "aocl", "default"), "Generating done")
            self.test_build()
            self.test_config((True, "openblas", "openblas", "default"), "Generating done")
            self.test_build()
            self.test_config((True, "netlib", "netlib", "default"), "Generating done")
            self.test_build()


class MiscTest(BaseTest):
    def gen_config(self) -> BuildConfig:
        config = gen_constant_config()
        config.set_compiler_vendor(self.vendor)
        config.build_shared_libs = self.build_shared_libs
        config.with_mpi = self.flags[0]
        config.with_cuda = self.flags[1]
        config.with_hdf5 = self.flags[2]
        config.with_feast = False
        config.eigen_backend = "default"
        config.diag_backend = "default"
        config.tbpm_backend = "default"
        return config

    def run(self) -> None:
        vendors = ("gnu", "clang")
        build_shared = (True, False)
        options = (True, False)
        for k in itertools.product(vendors, build_shared):
            self.vendor = k[0]
            self.build_shared_libs = k[1]
            for opt in itertools.product(options, options, options):
                self.test_config(opt, "Generating done")
                self.test_build()

        
def main():
    MathLibTest().run()
    MiscTest().run()


if __name__ == "__main__":
    main()
