"""
Differents functions needed to create the provider to access the R language
This functions were adapted using as a base the plugin Processing R Provider created by Victor Olaya in 2012
"""

import configparser
import os
import pathlib
import platform
import re
import subprocess
import sys
import shutil
import ctypes
import json
from typing import Optional

from processing.core.ProcessingConfig import ProcessingConfig
from processing.tools.system import mkdir, userFolder
from qgis.core import Qgis, QgsMessageLog, QgsProcessingUtils, QgsApplication, QgsProject, QgsRasterLayer
from qgis.PyQt.QtCore import QCoreApplication, QSettings

DEBUG = True
profile_home = QgsApplication.qgisSettingsDirPath()
path = os.path.exists(os.path.join(profile_home, 'python', 'plugins', 'processing_r'))
if path:
    try:
        from processing_r.processing.utils import RUtils
    except Exception:
        pass

class ParamsR:
    """Utilities for the R Provider and Algorithm"""

    """Definition of the parameters"""
    DOSSIER_SCRIPTS_R = "DOSSIER_SCRIPTS_R"
    DOSSIER_R = "DOSSIER_R"
    R64 = "R64"
    LIBS_R = "LIBS_R"
    USER_LIB = "USER_LIB"
    DEPOT_R = "DEPOT_R"

    VALID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

    @staticmethod
    def is_windows():
        """
        Returns True if the plugin is running on Windows

        :returns: Detection result
        :rtype: bool
        """
        return os.name == "nt"

    @staticmethod
    def is_macos() -> bool:
        """
        Returns True if the plugin is running on MacOS

        :returns: Detection result
        :rtype: bool
        """
        return platform.system() == "Darwin"
    
    @staticmethod
    def locate_r(target):
        """
        Detection of R folder and librairies folder

        :param target: Name of the element to search (R => R folder, Rscript => Librairies folder)
        :type target: str

        :returns: Folder desired
        :rtype: str
        """
        
        """Method 1 for the ocalisation of different possible paths"""
        r_exec = shutil.which(target)
        if r_exec:
            if "grass" in r_exec.lower() or r_exec.lower().endswith("r.bat"):
                r_exec = None
            elif r_exec.lower().endswith("r.exe"):
                r_exec = r_exec
            else:
                r_exec = None
        
        """Method 2 if first failed"""
        if not r_exec:
            system = platform.system()
            possible_paths = []

            # Detection with windows
            if system == "Windows":
                possible_paths = [
                    r"C:\Program Files\R",
                    r"C:\Program Files (x86)\R",
                    r"C:\R"
                ]
                for base in possible_paths:
                    if not os.path.isdir(base):
                        continue

                    for sub in os.listdir(base):
                        path = os.path.join(base, sub, "bin", "R.exe")
                        if os.path.isfile(path):
                            r_exec = path
                            break

                    if r_exec:
                        break
            # Detection with MacOS                
            elif system == "Darwin":
                possible_paths = [
                    "/Library/Frameworks/R.framework/Resources/bin/R",
                    "/usr/local/bin/R"
                ]
                for candidate in possible_paths:
                    if os.path.exists(candidate):
                        r_exec = candidate
                        break
            # Detection with Linux
            else: 
                possible_paths = [
                    "/usr/bin/R",
                    "/usr/local/bin/R"
                ]
                for candidate in possible_paths:
                    if os.path.exists(candidate):
                        r_exec = candidate
                        break
                        
        # Detection if "False R" linked to GRASS
        if r_exec and os.path.basename(r_exec).lower() in ["r.bat", "r.cmd"]:
            try:
                out = subprocess.check_output([r_exec, "--version"], stderr=subprocess.DEVNULL)
                out = out.decode().lower()
                if "grass" in out or "qgis" in out:
                    r_exec = None
            except Exception:
                r_exec = None
        
        # If not results, return None
        if not r_exec:
            return None

        """If path found"""
        # Target and commande to locate the folder(s)
        if target == "R":
            target = "RHOME"
            cmd = "cat(.libPaths()[1])"
        else:
            target = "-s"
            cmd = "cat(paste(.libPaths(), collapse='|'))"

        # Finding the path depending of the OS
        if platform.system() == "Windows":
            si = subprocess.STARTUPINFO()
            si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
            try:
                folder = subprocess.check_output(
                    [r_exec, target, "-e", cmd],
                    universal_newlines=True, stderr=subprocess.DEVNULL,
                    text=True, startupinfo=si, encoding = "utf-8"
                    ).strip()
            except:
                try:
                    folder = subprocess.check_output(
                        [r_exec, target, "-e", cmd],
                        universal_newlines=True, stderr=subprocess.DEVNULL,
                        text=True, startupinfo=si, encoding = "ISO-8859-1"
                        ).strip()
                except:
                    return None
        else:
            try:
                folder = subprocess.check_output(
                    [r_exec, target, "-e", cmd],
                    universal_newlines=True, stderr=subprocess.DEVNULL,
                    text=True, encoding = "utf-8"
                ).strip()
            except:
                try:
                    folder = subprocess.check_output(
                        [r_exec, target, "-e", cmd],
                        universal_newlines=True, stderr=subprocess.DEVNULL,
                        text=True, encoding = "ISO-8859-1"
                    ).strip()
                except:
                    return None

        # Correction for the path to R librairies due to the query mode
        if target == "-s":
            folder = folder.strip().split("\n")[-1]
            try:
                candidates = folder.strip().split('|')
            except Exception:
                lib_paths = []
            lib_paths = [os.path.normpath(p) for p in candidates]
        else:
            lib_paths = [folder]

        # Case if text with the path to only extract it
        if system == "Windows":
            fixed = []
            for folder in lib_paths:
                # Specific case where text is mixed with the path
                pattern = (
                r"[A-Za-z]:[\\/](?:[^\\/:*?\"<>|\r\n]+[\\/]?)*"
                r"|/(?:[^/\s><]+/)*[^/\s><]*"
                )
                paths = re.findall(pattern, folder)
                for path in paths:
                    buf = ctypes.create_unicode_buffer(260)
                    if ctypes.windll.kernel32.GetLongPathNameW(path, buf, 260) != 0:
                        fixed.append(buf.value)
                    else:
                        fixed.append(path)
            lib_paths = fixed

        # Checking the folder with biggest number of files to locate the used libraries 
        max_count = -1
        for lp in lib_paths:
            try:
                count = len([
                    d for d in os.listdir(lp)
                    if os.path.isdir(os.path.join(lp, d))
                ])
            except Exception:
                count = -1
            if count > max_count:
                max_count = count
                folder = lp

        # Return folder detected, or None
        if folder: 
            if target != 'RHOME':
                try:
                    test = os.path.join(folder, ".write_test")
                    open(test, "w").close()
                    os.remove(test)
                except Exception:
                    return None
            return folder
        else:
            return None

    @staticmethod
    def r_binary_folder():
        """
        Returns the folder (hopefully) containing R binaries

        :returns: Path to the binary folder
        :rtype: str
        """
        # Case 0: The choice was already defined
        folder = QSettings().value("Siligites/DOSSIER_R")

        # Case 1: The path was already defined in the provider
        if not folder:
            # print("1A") # debugtest
            folder = ProcessingConfig.getSetting(ParamsR.DOSSIER_R)

        # Case 2: The path is already defined in Processing R Provider
        if not folder:
            # Case 2: The path is already defined in Processing R Provider
            if path:
                # print("1B") # debugtest
                folder = ProcessingConfig.getSetting(RUtils.R_FOLDER)

        # Case 3: The path needs to be detected
        if not folder:
            # print("1C") # debugtest
            # Sub-case A: Using our own function for detection
            folder = ParamsR.locate_r("R")
            # Sub-case B: Using the original function
            if folder in ["", None]:
                # print("1D") # debugtest
                folder = ParamsR.guess_r_binary_folder()

        return os.path.abspath(folder) if folder else ""

    @staticmethod
    def guess_r_binary_folder():
        """
        Tries to pick a reasonable path for the R binaries to be executed from

        :returns: Possible path to the binary folder
        :rtype: str
        """
        if ParamsR.is_macos():
            return "/usr/local/bin"

        if ParamsR.is_windows():
            search_paths = ["ProgramW6432", "PROGRAMFILES(x86)", "PROGRAMFILES", "C:\\"]
            r_folder = ""
            for path in search_paths:
                if path in os.environ and os.path.isdir(os.path.join(os.environ[path], "R")):
                    r_folder = os.path.join(os.environ[path], "R")
                    break

            if r_folder:
                sub_folders = os.listdir(r_folder)
                sub_folders.sort(reverse=True)
                for sub_folder in sub_folders:
                    if sub_folder.upper().startswith("R-"):
                        return os.path.join(r_folder, sub_folder)

        # expect R to be in OS path
        return ""

    @staticmethod
    def package_repo():
        """
        Returns the package repo URL

        :returns: Path to repository
        :rtype: str
        """
        return ProcessingConfig.getSetting(ParamsR.DEPOT_R)

    @staticmethod
    def use_user_library():
        """
        Returns True if user library folder should be used instead of system folder

        :returns: Choice if the user want to use is own folder
        :rtype: bool
        """
        return ProcessingConfig.getSetting(ParamsR.USER_LIB)

    @staticmethod
    def r_library_folder():
        """
        Returns the user R library folder

        :returns: Path to the libraries folder
        :rtype: str
        """
        # Case 0: The choice was already defined
        folder = QSettings().value("Siligites/LIBS_R")

        # Case 1: The path was already defined in the provider
        if not folder:
            # print("2A") # debugtest
            folder = ProcessingConfig.getSetting(ParamsR.LIBS_R)
        
        # Case 2: The path is already defined in Processing R Provider
        if not folder:
            if path:
                # print("2B") # debugtest
                folder = ProcessingConfig.getSetting(RUtils.R_LIBS_USER)

        # Case 3: The path needs to be detected
        if not folder:
            # print("2C") # debugtest
            # Sub-case A: Using our own function for detection
            folder = ParamsR.locate_r("Rscript")
            # Sub-case B: Creation of a folder to receive the libraries
            if not folder or folder == "":
                # print("2D") # debugtest
                folder = str(os.path.join(userFolder(), "rlibs"))
                if not os.path.exists(folder):
                    mkdir(folder)

        return os.path.abspath(str(folder))

    @staticmethod
    def builtin_scripts_folder():
        """
        Returns the built-in scripts path

        :returns: Path to the scripts folder
        :rtype: str
        """
        return os.path.join(os.path.dirname(__file__), "rscripts")

    @staticmethod
    def default_scripts_folder():
        """
        Returns the default path to look for user scripts within

        :returns: Path to the scripts folder
        :rtype: str
        """
        folder = os.path.join(userFolder(), "rscripts")
        if not os.path.exists(folder):
            mkdir(folder)
        return os.path.abspath(folder)

    @staticmethod
    def script_folders():
        """
        Returns a list of folders to search for scripts within

        :returns: Path to the scripts folder
        :rtype: str
        """
        # Case 0: The choice was already defined
        folder = QSettings().value("Siligites/DOSSIER_SCRIPTS_R")

        # Case 1: The path was already defined in the provider
        if not folder:
            # print("3A") # debugtest
            folder = ProcessingConfig.getSetting(ParamsR.DOSSIER_SCRIPTS_R)

        # Case 2: The path is already defined in Processing R Provider
        if not folder:
            if path:
                # print("3B") # debugtest
                folder = ProcessingConfig.getSetting(RUtils.RSCRIPTS_FOLDER)

        if folder is not None:
            folders = folder.split(";")

        # Case 3: The path needs to be detected
        else:
            # print("3C") # debugtest
            folders = [ParamsR.default_scripts_folder()]

        folders.append(ParamsR.builtin_scripts_folder())
        return folders
    
    @staticmethod
    def user_libs():
        """
        Returns the choice to use the user R librairies or not

        :returns: Choice if the user want to use is own folder
        :rtype: bool
        """
        # Case 0: The choice was already defined
        choice = QSettings().value("Siligites/USER_LIB")

        # Case 1: The choice was already defined in the provider
        if not choice:
            choice = ProcessingConfig.getSetting(ParamsR.USER_LIB)

        # Case 2: The choice is already defined in Processing R Provider
        if not choice:
            if path:
                choice = ProcessingConfig.getSetting(RUtils.R_USE_USER_LIB)

        # Case 3: Default choice
        if not choice:
            choice = False
            
        return choice
            
    @staticmethod
    def repository():
        """
        Returns the path to the R repository

        :returns: Path to repository
        :rtype: str
        """
        # Case 0: The choice was already defined
        repository = QSettings().value("Siligites/DEPOT_R")

        # Case 1: The choice was already defined in the provider
        if not repository:
            repository = ProcessingConfig.getSetting(ParamsR.DEPOT_R)

        # Case 2: The choice is already defined in Processing R Provider
        if not repository:
            if path:
                repository = ProcessingConfig.getSetting(RUtils.R_REPO)

        # Case 3: Default choice
        if not repository:
            repository = "http://cran.at.r-project.org/"

        return repository
    
    @staticmethod
    def R_64():
        """
        Returns the choice to the 64bit version for Windows

        :returns: Choice if the user want to use x64 version
        :rtype: bool
        """
        # Case 0: The choice was already defined
        choice = QSettings().value("Siligites/R64")

        # Case 1: The choice was already defined in the provider
        if not choice:
            choice = ProcessingConfig.getSetting(ParamsR.R64)

        # Case 2: The choice is already defined in Processing R Provider
        if not choice:
            if path:
                choice = ProcessingConfig.getSetting(RUtils.R_USE64)

        # Case 3: Default choice
        if not choice:
            choice = True
            
        return choice

    @staticmethod
    def create_descriptive_name(name):
        """
        Returns a safe version of a parameter name

        :param name: String with possible problem
        :return: str

        :returns: Name corrected
        :rtype: str
        """
        return name.replace("_", " ")

    @staticmethod
    def is_valid_r_variable(variable):
        """
        Check if given string is valid R variable name.
        
        :param variable: Name to check
        :type variable: str

        :returns: bool
        :rtypes: bool
        """

        # only letters a-z, A-Z, numbers, dot and underscore
        x = re.search("[a-zA-Z0-9\\._]+", variable)

        result = True

        if variable == x.group():
            # cannot start with number or underscore, or start with dot followed by number
            x = re.search("^[0-9|_]|^\\.[0-9]", variable)
            if x:
                result = False
        else:
            result = False

        return result

    @staticmethod
    def strip_special_characters(name):
        """
        Strips non-alphanumeric characters from a name

        :param name: String to correct
        :type name: str

        :returns: String corrected
        :rtypes: str
        """
        return "".join(c for c in name if c in ParamsR.VALID_CHARS)

    @staticmethod
    def create_r_script_from_commands(commands):
        """
        Creates an R script in a temporary location consisting of the given commands.

        :param commands: Commands for the R Script
        :type commands: str

        :returns: Path to the temporary script file
        :rtypes: str
        """
        script_file = QgsProcessingUtils.generateTempFilename("processing_script.r")

        with open(script_file, "w", encoding="utf8") as f:
            for command in commands:
                f.write(command + "\n")

        return script_file

    @staticmethod
    def is_error_line(line):
        """
        Returns True if the given line looks like an error message

        :param line: Possible error message
        :type line: str

        :returns: True error
        :rtypes: str
        """
        return any(
            [error in line for error in ["Error ", "Execution halted", "Error:"]]
        )

    @staticmethod
    def get_windows_code_page():
        """
        Determines MS-Windows CMD.exe shell codepage. Used into GRASS exec script under MS-Windows.

        :returns: Code page of Windows
        :rtypes: str
        """
        return str(ctypes.cdll.kernel32.GetACP())

    @staticmethod
    def get_process_startup_info():
        """
        Returns the correct startup info to use when calling commands for different platforms

        :returns: Info of the stratup process
        :rtypes: str
        """
        # For MS-Windows, we need to hide the console window.
        si = None
        if ParamsR.is_windows():
            si = subprocess.STARTUPINFO()
            si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
            si.wShowWindow = subprocess.SW_HIDE
        return si

    @staticmethod
    def get_process_keywords():
        """
        Returns the correct process keywords dict to use when calling commands for different platforms

        :returns: Process keywords
        :rtypes: str
        """
        kw = {}
        if ParamsR.is_windows():
            kw["startupinfo"] = ParamsR.get_process_startup_info()
            if sys.version_info >= (3, 6):
                kw["encoding"] = "cp{}".format(ParamsR.get_windows_code_page())

        return kw

    @staticmethod
    def execute_r_algorithm(alg, parameters, context, feedback):
        """
        Runs a prepared algorithm in R, and returns a list of the output received from R

        :param parameters: Parameters of the R algorithm
        :type parameters: str

        :param context: Context of the R algorithm translation
        :type context: str

        :param feedback: Feedback of the R algorithm
        :type feedback: str

        :returns: Console results
        :rtypes: str
        """
        # generate new R script file name in a temp folder

        script_lines = alg.build_r_script(parameters, context, feedback)
        for line in script_lines:
            if "st_write" in line or "writeRaster" in line:
                continue

            feedback.pushCommandInfo(line)

        script_filename = ParamsR.create_r_script_from_commands(script_lines)

        # run commands
        command = [ParamsR.path_to_r_executable(script_executable=True), script_filename]

        feedback.pushInfo(ParamsR.tr("Sortie de la console d'exécution R"))

        console_results = []

        with subprocess.Popen(
            command,
            stdout=subprocess.PIPE,
            stdin=subprocess.DEVNULL,
            stderr=subprocess.STDOUT,
            universal_newlines=True,
            **ParamsR.get_process_keywords()
        ) as proc:
            for line in iter(proc.stdout.readline, ""):
                if feedback.isCanceled():
                    proc.terminate()

                line = line.strip()
                handled = False

                if line.startswith("QGIS_ERROR:"):
                    feedback.reportError(line.replace("QGIS_ERROR:", "").strip())
                    handled = True

                elif line.startswith("QGIS_WARNING:"):
                    feedback.pushWarning(line.replace("QGIS_WARNING:", "").strip())
                    handled = True

                elif line.startswith("QGIS_INFO:"):
                    feedback.pushInfo(line.replace("QGIS_INFO:", "").strip())
                    handled = True

                elif line.startswith("OPTIONAL_OUTPUT="):
                    path = line.split("=", 1)[1].strip()
                    if os.path.exists(path):
                        name = ParamsR.tr("MNT en ligne")
                        layer = QgsRasterLayer(path, name)
                        if layer.isValid():
                            QgsProject.instance().addMapLayer(layer, False)

                if not handled:
                    if ParamsR.is_error_line(line):
                        feedback.reportError(line)
                    else:
                        feedback.pushConsoleInfo(line)

                console_results.append(line)

        return console_results

    @staticmethod
    def html_formatted_console_output(output):
        """
        Returns a HTML formatted string of the given output lines

        :param output: Output of the algorithm
        :type output: str

        :returns: HTML of the output algorithm
        :rtypes: str
        """
        s = "<h2>{}</h2>\n".format(ParamsR.tr("Sortie R"))
        s += "<code>\n"

        for line in output:
            s += "{}<br />\n".format(line)
        s += "</code>"

        return s

    @staticmethod
    def path_to_r_executable(script_executable=False) -> str:
        """
        Returns the path to the R executable

        :param script_executable: State of the possibility to execute the script
        :type script_executable: bool

        :returns: Executable to use
        :rtypes: str
        """
        executable = "Rscript" if script_executable else "R"
        bin_folder = ParamsR.r_binary_folder()
        if bin_folder:
            if ParamsR.is_windows():
                if ProcessingConfig.getSetting(ParamsR.R64):
                    exec_dir = "x64"
                else:
                    exec_dir = "i386"
                return os.path.join(bin_folder, "bin", exec_dir, "{}.exe".format(executable))
            return os.path.join(bin_folder, executable)

        return executable

    @staticmethod
    def check_r_is_installed():
        """
        Checks if R is installed and working. Returns None if R IS working, or an error string if R was not found.

        :returns: HTML of the R parameters
        :rtypes: str
        """
        if DEBUG:
            QgsMessageLog.logMessage(
                ParamsR.tr("Chemin binaire de R: {}").format(ParamsR.path_to_r_executable()), "R", Qgis.MessageLevel.Info
            )

        if ParamsR.is_windows():
            path = ParamsR.r_binary_folder()
            if path == "":
                message = "\n".join([ParamsR.tr("Le dossier R n'est pas configuré correctemnt"),
                        ParamsR.tr("Veuillez le configurer avant d'exécuter les scripts R")
                    ])
                return message

        command = [ParamsR.path_to_r_executable(), "--version"]
        try:
            with subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stdin=subprocess.DEVNULL,
                stderr=subprocess.STDOUT,
                universal_newlines=True,
                **ParamsR.get_process_keywords()
            ) as proc:
                for line in proc.stdout:
                    if ("R version" in line) or ("R Under development" in line):
                        return None
        except FileNotFoundError:
            pass

        html = "\n".join([ParamsR.tr("Malheureusement, il semble que R ne soit pas installé sur votre système ou qu'il ne soit pas correctement configuré pour être utilisé depuis QGIS"),
                "http://docs.qgis.org/testing/en/docs/user_manual/processing/3rdParty.html",
                ParamsR.tr("Suivre ce lien pour en savoir plus sur l'installation et la configuration de R pour l'utiliser avec QGIS")
            ])
        
        return html

    @staticmethod
    def get_r_version():
        """
        Returns the current installed R version, or None if R is not found

        :returns: Installed R version
        :rtypes: str
        """
        if ParamsR.is_windows() and not ParamsR.r_binary_folder():
            return None

        command = [ParamsR.path_to_r_executable(), "--version"]
        try:
            with subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stdin=subprocess.DEVNULL,
                stderr=subprocess.STDOUT,
                universal_newlines=True,
                **ParamsR.get_process_keywords()
            ) as proc:
                for line in proc.stdout:
                    if ("R version" in line) or ("R Under development" in line):
                        return line
        except FileNotFoundError:
            pass

        return None

    @staticmethod
    def get_required_packages(code):
        """
        Returns a list of the required packages

        :param code: R script code
        :type code: str

        :returns: packages used in the code
        :rtypes: str
        """
        regex = re.compile(r'[^#]library\("?(.*?)"?\)')
        return regex.findall(code)

    @staticmethod
    def upgrade_parameter_line(line: str):
        """
        Upgrades a parameter definition line from 2.x to 3.x format
        
        :param line: Line to redefine
        :type line: str

        :returns: Redefined line
        :rtypes: str
        """
        # alias 'selection' to 'enum'
        if "=selection" in line:
            line = line.replace("=selection", "=enum")

        if "=vector" in line:
            line = line.replace("=vector", "=source")

        return line

    @staticmethod
    def tr(string):
        return QCoreApplication.translate("SiliciteR", string)


def log(message: str):
    """
    Simple logging function, most for debuging.

    :param message: Message for the log
    :type message: str
    """
    QgsMessageLog.logMessage(message, "SiliciteR", Qgis.MessageLevel.Info)


def _read_metadata():
    """
    Read metadata file.

    :returns: Read Metadata
    :rtypes: configparser.ConfigParser
    """
    path = pathlib.Path(__file__).parent.parent / "metadata.txt"

    config = configparser.ConfigParser()
    config.read(path)

    return config


def plugin_version():
    """
    Get plugin version.

    :returns: Plugin version
    :rtypes: str
    """
    config = _read_metadata()
    return config["general"]["version"]
