import sys
import os
import shutil
import numpy as np
from pathlib import Path
from time import sleep
import multiprocessing
import ruamel.yaml
import warnings
import gmsh


class FilesAndFolders:
    @staticmethod
    def read_data_from_yaml(full_file_path, data_class):
        with open(full_file_path, 'r') as stream:
            yaml = ruamel.yaml.YAML(typ='safe', pure=True)
            yaml_str = yaml.load(stream)
        return data_class(**yaml_str)

    @staticmethod
    def write_data_to_yaml(full_file_path, dict_of_data_class):
        def my_represent_none(self, data):
            return self.represent_scalar('tag:yaml.org,2002:null', 'null')

        yaml = ruamel.yaml.YAML()
        yaml.default_flow_style = False
        yaml.emitter.alt_null = 'Null'
        yaml.representer.add_representer(type(None), my_represent_none)
        with open(full_file_path, 'w') as yaml_file:
            yaml.dump(dict_of_data_class, yaml_file)

    @staticmethod
    def prep_folder(folder_full_path, clear: bool = False):
        if clear:
            if os.path.exists(folder_full_path):
                shutil.rmtree(folder_full_path)  # delete directory
        if not os.path.exists(folder_full_path):
            os.makedirs(folder_full_path)  # make new directory

    @staticmethod
    def get_folder_path(folder_type, folder, ref_nr, overwrite, required_folder):
        if required_folder and not (ref_nr and overwrite):
            last_nr = 0
            for study in [x.parts[-1] for x in Path(folder).iterdir() if x.is_dir()]:
                last_nr = max(int(study[study.find('_') + 1:]), last_nr)
            if overwrite and required_folder and last_nr > 0:
                run_nr = str(last_nr)
            else:
                run_nr = str(last_nr + 1)
        else:
            run_nr = str(ref_nr)

        folder_path = os.path.join(folder, folder_type + '_' + run_nr)
        FilesAndFolders.prep_folder(folder_path, overwrite and required_folder)
        return folder_path

    @staticmethod
    def print_welcome_graphics():
        print(r"  _____ _  ___        ____ ")
        print(r"|  ___(_)/ _ \ _   _/ ___| ")
        print(r"| |_  | | | | | | | \___ \ ")
        print(r"|  _| | | |_| | |_| |___) |")
        print(r"|_|   |_|\__\_\\__,_|____/ ")
        print("")


class CheckForExceptions:
    @staticmethod
    def check_inputs(run):  # RunFiQuS()
        if run.type == 'start_from_yaml':
            if run.geometry and not run.overwrite:
                warnings.warn("Warning: Geometry folder is needed only if it has to be overwritten. Ignoring it...")
            if run.solution or run.mesh:
                warnings.warn("Warning: Mesh and Solution folders are not needed. Ignoring them...")
        elif run.type == 'geometry_only':
            if run.solution or run.mesh:
                warnings.warn("Warning: Mesh and Solution folders are not needed. Ignoring them...")
        elif run.type == 'mesh_and_solve_with_post_process':
            if not run.geometry:
                raise Exception('Full path to Geometry not provided. '
                                'Insert options -> reference_files -> geometry.')
            if run.mesh and not run.overwrite:
                warnings.warn("Warning: Mesh folder is needed only if it has to be overwritten. Ignoring it...")
            if run.solution:
                warnings.warn("Warning: Solution folder is not needed. Ignoring it...")
        elif run.type == 'mesh_only':
            if not run.geometry:
                raise Exception('Full path to Mesh not provided. '
                                'Insert options -> reference_files -> geometry.')
            if run.solution:
                warnings.warn("Warning: Solution folder is not needed. Ignoring it...")
        elif run.type == 'solve_with_post_process':
            if not run.mesh or not run.geometry:
                raise Exception('Full path to Mesh not provided. '
                                'Insert options -> reference_files -> geometry and mesh.')
            if run.solution and not run.overwrite:
                warnings.warn("Warning: Solution folder is needed only if it has to be overwritten. Ignoring it...")
        elif run.type == 'solve_only':
            if not run.mesh or not run.geometry:
                raise Exception('Full path to Mesh not provided. '
                                'Insert options -> reference_files -> geometry and mesh.')
            if run.solution and not run.overwrite:
                warnings.warn("Warning: Solution folder is needed only if it has to be overwritten. Ignoring it...")
        elif run.type == 'post_process_only':
            if not run.mesh or not run.geometry or not run.solution:
                raise Exception('Full path to Solution not provided. '
                                'Insert options -> reference_files -> geometry, mesh, and solution.')

    @staticmethod
    def check_overwrite_conditions(folder_type, folder, ref_nr):
        if ref_nr:
            run_nr = str(ref_nr)
            if not os.path.exists(os.path.join(folder, folder_type + '_' + run_nr)):
                warnings.warn(
                    f'The folder {folder_type}_{run_nr} does not exist. Creating it...')
        else:
            warnings.warn(
                f'Reference number of the folder {folder_type} not provided. '
                f'Overwriting the latest {folder_type} folder...')


class GeometricFunctions:

    @staticmethod
    def centroid(X, Y):
        """
            Computes the centroid coordinates of a non-self-intersecting closed polygon
            :param X: list of X coordinate of the vertices
            :param Y: list of y coordinate of the vertices
        """
        sum_A, sum_Cx, sum_Cy = 0, 0, 0
        for i in range(len(X)):
            index = i + 1 if i != len(X) - 1 else 0
            A = X[i] * Y[index] - X[index] * Y[i]
            sum_Cx += (X[i] + X[index]) * A
            sum_Cy += (Y[i] + Y[index]) * A
            sum_A += A
        factor = 1 / (3 * sum_A)
        return [factor * sum_Cx, factor * sum_Cy]

    @staticmethod
    def arc_center_from_3_points(a, b, c):
        """
            Computes the center coordinates of an arc passing through three points
            :param a: DataRoxieParser.Coord class object of one arc point
            :param b: DataRoxieParser.Coord class object of one arc point
            :param c: DataRoxieParser.Coord class object of one arc point
        """
        ab = [a.x - b.x, a.y - b.y]
        ac = [a.x - c.x, a.y - c.y]
        sac = [a.x * a.x - c.x * c.x, a.y * a.y - c.y * c.y]
        sba = [b.x * b.x - a.x * a.x, b.y * b.y - a.y * a.y]
        yy = (sac[0] * ab[0] + sac[1] * ab[0] + sba[0] * ac[0] + sba[1] * ac[0]) / \
             (2 * ((c.y - a.y) * ab[0] - (b.y - a.y) * ac[0]))
        xx = (sac[0] * ab[1] + sac[1] * ab[1] + sba[0] * ac[1] + sba[1] * ac[1]) / \
             (2 * ((c.x - a.x) * ab[1] - (b.x - a.x) * ac[1]))
        return [-xx, -yy]


class GmshUtils:

    def __init__(self, model_name=None, verbose=True):
        self.model_name = model_name
        self.verbose = verbose

    @staticmethod
    def initialize():
        if not gmsh.is_initialized():
            gmsh.initialize(sys.argv)
            num_threads = multiprocessing.cpu_count()
            gmsh.option.setNumber('General.NumThreads', num_threads)  # enable multithreading (this seems to be only for meshing)
            gmsh.option.setNumber('Mesh.MaxNumThreads1D', num_threads)
            gmsh.option.setNumber('Mesh.MaxNumThreads2D', num_threads)
            gmsh.option.setNumber('Mesh.MaxNumThreads3D', num_threads)

            gmsh.option.setNumber('Geometry.ToleranceBoolean', 0.0000001)
            gmsh.option.setNumber('General.Terminal', 1)
            # gmsh.model.add(self.model_name)

    def check_for_event(self):  # pragma: no cover
        action = gmsh.onelab.getString("ONELAB/Action")
        if len(action) and action[0] == "check":
            gmsh.onelab.setString("ONELAB/Action", [""])
            if self.verbose:
                print("-------------------check----------------")
            gmsh.fltk.update()
            gmsh.graphics.draw()
        if len(action) and action[0] == "compute":
            gmsh.onelab.setString("ONELAB/Action", [""])
            if self.verbose:
                print("-------------------compute----------------")
            gmsh.onelab.setChanged("Gmsh", 0)
            gmsh.onelab.setChanged("GetDP", 0)
            gmsh.fltk.update()
            gmsh.graphics.draw()
        return True

    def launch_interactive_GUI(self, close_after=-1):  # pragma: no cover
        gmsh.fltk.initialize()
        while gmsh.fltk.isAvailable() and self.check_for_event():
            gmsh.fltk.wait()
            if close_after >= 0:
                sleep(close_after)
                gmsh.fltk.finalize()
        gmsh.finalize()


class RoxieParsers:
    def __init__(self, conductor, block, xyCorner):
        self.conductor = conductor
        self.block = block
        self.xyCorner = xyCorner

    @staticmethod
    def parseMap2d(map2dFile: Path, headerLines: int = 1):
        """
            Generates array-stream of values of map2dFile
            :param map2dFile: path of map2dFile containing the content to parse
            :param headerLines: which index the header line is at - will start to read after that
        """
        # Open map2dfile
        fileContent = open(map2dFile, "r").read()
        # Split content of file in rows
        fileContentByRow = fileContent.split("\n")
        # Create array-matrix to fill in with the values of the file
        output_matrix = np.array([[None for x in range(10)] for y in range(headerLines + 1, len(fileContentByRow) - 1)],
                                 dtype=float)

        # Assign values to the matrix row by row
        for index, rowContent in enumerate(fileContentByRow):
            if index > headerLines and rowContent:  # without header
                row = rowContent.split()
                output_array = np.array([])  # create temp. array
                output_array = np.append(output_array, int(row[0]))  # strands to groups
                output_array = np.append(output_array, int(row[1]))  # strands to halfturn
                output_array = np.append(output_array, float(row[2]))  # idx
                output_array = np.append(output_array, float(row[3]) / 1e3)  # x_strands in [m]
                output_array = np.append(output_array, float(row[4]) / 1e3)  # y_strands in [m]
                output_array = np.append(output_array, float(row[5]))  # Bx
                output_array = np.append(output_array, float(row[6]))  # By
                output_array = np.append(output_array, float(row[7]) / 1e6)  # Area in [m^2]
                output_array = np.append(output_array, float(row[8]))  # I_strands
                output_array = np.append(output_array, float(row[9]))  # fill factor
                output_matrix[index - headerLines - 1] = output_array  # assign into matrix
        return output_matrix

    @staticmethod
    def parseCond2d(cond2dFile: Path):
        """
            Read input file and return list of ConductorPosition objects

            # input: fileName
            # output: conductorPositionsList

        """
        # conductorStartKeyword = "CONDUCTOR POSITION IN THE CROSS-SECTION"
        blockStartKeyword = "BLOCK POSITION IN THE CROSS-SECTION"

        fileContent = open(cond2dFile, "r").read()

        # separate rows
        fileContentByRow = fileContent.split("\n")

        # Find block definition
        for i in range(len(fileContentByRow)):
            if blockStartKeyword in fileContentByRow[i]:
                startOfBlockDefinitionIndex = i

        # separate part of the data with conductor position information
        conductorPositions = fileContentByRow[5:startOfBlockDefinitionIndex - 2]

        # drop every 5th row
        conductorPositionsFourVertices = list(conductorPositions)
        del conductorPositionsFourVertices[4::5]

        # arrange data in a list of lists
        outputConductorPositions = []
        for row in conductorPositionsFourVertices:
            rowSplitStr = row.split(',')
            rowSplitFloat = [float(elem) for elem in rowSplitStr]
            outputConductorPositions.append(rowSplitFloat)

        # arrange data from list to numpy.array
        outputConductorPositionsMatrix = np.array(outputConductorPositions)

        # input: outputConductorPositions
        # output: conductorPositionsList
        conductorPositionsList = []
        for i in range(0, len(outputConductorPositions), 4):
            out = outputConductorPositions[i]
            conductor = int(out[1])
            block = int(out[2])
            xyCorner = outputConductorPositionsMatrix[i:i + 4, 4:6]
            conductorPositionsList.append(RoxieParsers(conductor, block, xyCorner))

        return conductorPositionsList
