import os
import copy
from datetime import date
import locale
import itertools
import numpy as np
from scipy import interpolate

from PySide2 import QtGui, QtCore, QtWidgets

import PyAero
import GraphicsItemsCollection as gic
import GraphicsItem
import Connect
from Utils import Utils
from Settings import OUTPUTDATA
import logging
logger = logging.getLogger(__name__)


class Windtunnel:
    """docstring for Windtunnel"""

    def __init__(self):

        # contains list of BlockMesh objects
        self.blocks = []

        # get MainWindow instance (overcomes handling parents)
        self.mainwindow = QtCore.QCoreApplication.instance().mainwindow

    def AirfoilMesh(self, name='', contour=None, divisions=15, ratio=3.0,
                    thickness=0.04):

        # get airfoil contour coordinates
        x, y = contour

        # make a list of point tuples
        # [(x1, y1), (x2, y2), (x3, y3), ... , (xn, yn)]
        line = list(zip(x, y))

        # block mesh around airfoil contour
        self.block_airfoil = BlockMesh(name=name)
        self.block_airfoil.addLine(line)

        # self.block_airfoil.extrudeLine(line, length=thickness, direction=3,
        #                                divisions=divisions, ratio=ratio)
        self.block_airfoil.extrudeLine_cell_thickness(line,
                                                      cell_thickness=thickness,
                                                      growth=ratio,
                                                      divisions=divisions,
                                                      direction=3)

        self.blocks.append(self.block_airfoil)

    def TrailingEdgeMesh(self, name='', te_divisions=3,
                         thickness=0.04, divisions=10, ratio=1.05):

        # compile first line of trailing edge block
        first = self.block_airfoil.getLine(number=0, direction='v')
        last = self.block_airfoil.getLine(number=-1, direction='v')
        last_reversed = copy.deepcopy(last)
        last_reversed.reverse()

        vec = np.array(first[0]) - np.array(last[0])
        line = copy.deepcopy(last_reversed)

        # in case of TE add the points from the TE
        if self.mainwindow.airfoil.has_TE:
            for i in range(1, te_divisions):
                p = last_reversed[-1] + float(i) / te_divisions * vec
                # p is type numpy.float, so convert it to float
                line.append((float(p[0]), float(p[1])))
            line += first
        # handle case with sharp trailing edge
        else:
            line += first[1:]

        # trailing edge block mesh
        block_te = BlockMesh(name=name)
        block_te.addLine(line)

        # block_te.extrudeLine(line, length=length, direction=4,
        #                      divisions=divisions, ratio=ratio)
        block_te.extrudeLine_cell_thickness(line,
                                            cell_thickness=thickness,
                                            growth=ratio,
                                            divisions=divisions,
                                            direction=4)

        # equidistant point distribution
        block_te.distribute(direction='u', number=-1)

        # make a transfinite interpolation
        # i.e. recreate pooints inside the block
        block_te.transfinite()

        self.block_te = block_te
        self.blocks.append(block_te)

    def TunnelMesh(self, name='', tunnel_height=2.0, divisions_height=100,
                   ratio_height=10.0, dist='symmetric'):
        block_tunnel = BlockMesh(name=name)

        self.tunnel_height = tunnel_height

        # line composed of trailing edge and airfoil meshes
        line = self.block_te.getVLines()[-1]
        line.reverse()
        del line[-1]
        line += self.block_airfoil.getULines()[-1]
        del line[-1]
        line += self.block_te.getVLines()[0]
        block_tunnel.addLine(line)

        # line composed of upper, lower and front line segments
        p1 = np.array((block_tunnel.getULines()[0][0][0], tunnel_height))
        p2 = np.array((0.0, tunnel_height))
        p3 = np.array((0.0, -tunnel_height))
        p4 = np.array((block_tunnel.getULines()[0][-1][0], -tunnel_height))

        # upper line of wind tunnel
        line = list()
        vec = p2 - p1
        for t in np.linspace(0.0, 1.0, 10):
            p = p1 + t * vec
            line.append(p.tolist())
        del line[-1]

        # front half circle of wind tunnel
        for phi in np.linspace(90.0, 270.0, 200):
            phir = np.radians(phi)
            x = tunnel_height * np.cos(phir)
            y = tunnel_height * np.sin(phir)
            line.append((x, y))
        del line[-1]

        # lower line of wind tunnel
        vec = p4 - p3
        for t in np.linspace(0.0, 1.0, 10):
            p = p3 + t * vec
            line.append(p.tolist())

        line = np.array(line)
        tck, u = interpolate.splprep(line.T, s=0, k=1)

        # point distribution on upper, front and lower part
        if dist == 'symmetric':
            ld = -1.3
            ud = 1.3
        if dist == 'lower':
            ld = -1.2
            ud = 1.5
        if dist == 'upper':
            ld = -1.5
            ud = 1.2
        xx = np.linspace(ld, ud, len(block_tunnel.getULines()[0]))
        t = (np.tanh(xx) + 1.0) / 2.0

        line = interpolate.splev(t, tck, der=0)
        line = list(zip(line[0].tolist(), line[1].tolist()))

        block_tunnel.addLine(line)

        p5 = np.array(block_tunnel.getULines()[0][0])
        p6 = np.array(block_tunnel.getULines()[0][-1])

        # first vline
        vline1 = BlockMesh.makeLine(p5, p1, divisions=divisions_height,
                                    ratio=ratio_height)

        # last vline
        vline2 = BlockMesh.makeLine(p6, p4, divisions=divisions_height,
                                    ratio=ratio_height)

        boundary = [block_tunnel.getULines()[0],
                    block_tunnel.getULines()[-1],
                    vline1,
                    vline2]
        block_tunnel.transfinite(boundary=boundary)

        # blending between normals (inner lines) and transfinite (outer lines)
        ulines = list()
        old_ulines = block_tunnel.getULines()

        for j, uline in enumerate(block_tunnel.getULines()):

            # skip first and last line
            if j == 0 or j == len(block_tunnel.getULines()) - 1:
                ulines.append(uline)
                continue

            line = list()
            xo, yo = list(zip(*old_ulines[0]))
            xo = np.array(xo)
            yo = np.array(yo)
            normals = BlockMesh.curveNormals(xo, yo)

            for i, point in enumerate(uline):

                # skip first and last point
                if i == 0 or i == len(uline) - 1:
                    line.append(point)
                    continue

                pt = np.array(old_ulines[j][i])
                pto = np.array(old_ulines[0][i])
                vec = pt - pto
                # projection of vec into normal
                dist = np.dot(vec, normals[i]) / np.linalg.norm(normals[i])
                pn = pto + dist * normals[i]
                v = float(j) / float(len(block_tunnel.getULines()))
                exp = 0.6
                pnew = (1.0 - v**exp) * pn + v**exp * pt
                line.append((pnew.tolist()[0], pnew.tolist()[1]))

            ulines.append(line)

        block_tunnel = BlockMesh(name=name)
        for uline in ulines:
            block_tunnel.addLine(uline)

        ij = [0, 30, 0, len(block_tunnel.getULines()) - 1]
        block_tunnel.transfinite(ij=ij)
        ij = [len(block_tunnel.getVLines()) - 31,
              len(block_tunnel.getVLines()) - 1,
              0,
              len(block_tunnel.getULines()) - 1]
        block_tunnel.transfinite(ij=ij)

        sm = 1
        if sm == 1:
            smooth = Smooth(block_tunnel)

            nodes = smooth.selectNodes(domain='interior')
            block_tunnel = smooth.smooth(nodes, iterations=1,
                                         algorithm='laplace')
            ij = [1, 30, 1, len(block_tunnel.getULines()) - 2]
            nodes = smooth.selectNodes(domain='ij', ij=ij)
            block_tunnel = smooth.smooth(nodes, iterations=2,
                                         algorithm='laplace')
            ij = [len(block_tunnel.getVLines()) - 31,
                  len(block_tunnel.getVLines()) - 2,
                  1,
                  len(block_tunnel.getULines()) - 2]
            nodes = smooth.selectNodes(domain='ij', ij=ij)
            block_tunnel = smooth.smooth(nodes, iterations=3,
                                         algorithm='laplace')

        self.block_tunnel = block_tunnel
        self.blocks.append(block_tunnel)

    def TunnelMeshWake(self, name='', tunnel_wake=2.0,
                       divisions=100, ratio=0.1, spread=0.4):

        chord = 1.0

        block_tunnel_wake = BlockMesh(name=name)

        # line composed of trailing edge and block_tunnel meshes
        line = self.block_tunnel.getVLines()[-1]
        line.reverse()
        del line[-1]
        line += self.block_te.getULines()[-1]
        del line[-1]
        line += self.block_tunnel.getVLines()[0]
        block_tunnel_wake.addLine(line)

        #
        p1 = np.array((self.block_te.getULines()[-1][0][0],
                       self.tunnel_height))
        p4 = np.array((self.block_te.getULines()[-1][-1][0],
                       - self.tunnel_height))
        p7 = np.array((tunnel_wake + chord, self.tunnel_height))
        p8 = np.array((tunnel_wake + chord, -self.tunnel_height))

        upper = BlockMesh.makeLine(p7, p1, divisions=divisions,
                                   ratio=1.0 / ratio)
        lower = BlockMesh.makeLine(p8, p4, divisions=divisions,
                                   ratio=1.0 / ratio)
        left = line
        right = BlockMesh.makeLine(p8, p7, divisions=len(left) - 1, ratio=1.0)

        boundary = [upper, lower, right, left]
        block_tunnel_wake.transfinite(boundary=boundary)

        # equalize division line in wake
        for i, u in enumerate(block_tunnel_wake.getULines()[0]):
            if u[0] < chord + tunnel_wake * spread:
                ll = len(block_tunnel_wake.getULines()[0])
                line_no = -ll + i
                break
        block_tunnel_wake.distribute(direction='v', number=line_no)

        # transfinite left of division line
        ij = [len(block_tunnel_wake.getVLines()) + line_no,
              len(block_tunnel_wake.getVLines()) - 1,
              0,
              len(block_tunnel_wake.getULines()) - 1]
        block_tunnel_wake.transfinite(ij=ij)

        # transfinite right of division line
        ij = [0,
              len(block_tunnel_wake.getVLines()) + line_no,
              0,
              len(block_tunnel_wake.getULines()) - 1]
        block_tunnel_wake.transfinite(ij=ij)

        self.block_tunnel_wake = block_tunnel_wake
        self.blocks.append(block_tunnel_wake)

    def makeMesh(self):

        toolbox = self.mainwindow.centralwidget.toolbox

        if self.mainwindow.airfoil:
            if not hasattr(self.mainwindow.airfoil, 'spline_data'):
                message = 'Splining needs to be done first.'
                self.mainwindow.slots.messageBox(message)
                return

            contour = self.mainwindow.airfoil.spline_data[0]

        else:
            self.mainwindow.slots.messageBox('No airfoil loaded.')
            return

        # delete blocks outline if existing
        # because a new one will be generated
        if hasattr(self.mainwindow.airfoil, 'mesh_blocks'):
            self.mainwindow.scene.removeItem(
                self.mainwindow.airfoil.mesh_blocks)
            del self.mainwindow.airfoil.mesh_blocks

        progdialog = QtWidgets.QProgressDialog(
            "", "Cancel", 0, 100, self.mainwindow)
        progdialog.setFixedWidth(300)
        progdialog.setMinimumDuration(0)
        progdialog.setWindowTitle('Generating the CFD mesh')
        progdialog.setWindowModality(QtCore.Qt.WindowModal)
        progdialog.show()

        progdialog.setValue(10)
        # progdialog.setLabelText('making blocks')

        self.AirfoilMesh(name='block_airfoil',
                         contour=contour,
                         divisions=toolbox.points_n.value(),
                         ratio=toolbox.ratio.value(),
                         thickness=toolbox.normal_thickness.value())
        progdialog.setValue(20)

        if progdialog.wasCanceled():
            return

        self.TrailingEdgeMesh(name='block_TE',
                              te_divisions=toolbox.te_div.value(),
                              thickness=toolbox.length_te.value(),
                              divisions=toolbox.points_te.value(),
                              ratio=toolbox.ratio_te.value())
        progdialog.setValue(30)

        if progdialog.wasCanceled():
            return

        self.TunnelMesh(name='block_tunnel',
                        tunnel_height=toolbox.tunnel_height.value(),
                        divisions_height=toolbox.divisions_height.value(),
                        ratio_height=toolbox.ratio_height.value(),
                        dist=toolbox.dist.currentText())
        progdialog.setValue(50)

        if progdialog.wasCanceled():
            return

        self.TunnelMeshWake(name='block_tunnel_wake',
                            tunnel_wake=toolbox.tunnel_wake.value(),
                            divisions=toolbox.divisions_wake.value(),
                            ratio=toolbox.ratio_wake.value(),
                            spread=toolbox.spread.value() / 100.0)
        progdialog.setValue(70)

        if progdialog.wasCanceled():
            return

        # connect mesh blocks
        connect = Connect.Connect(progdialog)
        vertices, connectivity, progdialog = \
            connect.connectAllBlocks(self.blocks)

        # add mesh to Windtunnel instance
        self.mesh = vertices, connectivity

        # generate cell to vertex connectivity from mesh
        self.makeLCV()

        # generate cell to edge connectivity from mesh
        self.makeLCE()

        # generate boundaries from mesh connectivity
        unique, seen, doubles, boundary_edges = self.makeBoundaries()

        # find loops inside boundary_edges
        self.boundary_loops = self.findLoops(boundary_edges)

        logger.info('Mesh around {} created'.
                    format(self.mainwindow.airfoil.name))
        logger.info('Mesh has {} vertices and {} elements'.
                    format(len(vertices), len(connectivity)))

        self.drawMesh(self.mainwindow.airfoil)
        self.drawBlockOutline(self.mainwindow.airfoil)

        progdialog.setValue(100)

        # enable mesh export and set filename and boundary definitions
        toolbox.box_meshexport.setEnabled(True)

    def makeLCV(self):
        """Make cell to vertex connectivity for the mesh
           LCV is identical to connectivity
        """
        _, connectivity = self.mesh
        self.LCV = connectivity

    def makeLCE(self):
        """Make cell to edge connectivity for the mesh"""
        _, connectivity = self.mesh
        self.LCE = dict()
        self.edges = list()

        for i, cell in enumerate(connectivity):
            # example for Qudrilateral:
            # cell: [0, 1, 5, 4]
            # local_edges: [(0,1), (1,5), (5,4), (4,0)]
            local_edges = [(cell[j], cell[(j + 1) % len(cell)])
                           for j in range(len(cell))]

            # all edges for cell i
            self.LCE[i] = local_edges

            # all edges in one list
            self.edges += [tuple(sorted(edge)) for edge in local_edges]

    def makeLCC(self):
        """Make cell to cell connectivity for the mesh"""
        pass

    def makeBoundaries(self):
        """A boundary edge is an edge that belongs only to one cell"""
        seen = set()
        unique = list()
        doubles = set()
        for edge in self.edges:
            if edge not in seen:
                seen.add(edge)
                unique.append(edge)
            else:
                doubles.add(edge)

        boundary_edges = [edge for edge in unique if edge not in doubles]

        return unique, seen, doubles, boundary_edges

    def findLoops(self, edges):
        """Find loops in a list of edges which are stored in tuples
        and return the "connected components", in the disjoint set.
        In the case of boundary edges these are loops or
        "cycles" in graph theory language

        Args:
            edges (list of tuples):

        Returns:
            TYPE: Description
        """

        vertices, connectivity = self.mesh

        # make disjoint set object
        djs = DisjointSet()

        # add all edges to the disjoint set
        for edge in edges:
            djs.add(edge[0], edge[1])

        # get the boundary loops (airfoil, outer boundary)
        # djs.group returns a dictionary containing all loops
        # the key is an arbitrary node of the loop
        # the values per key are a list of unordered nodes
        # belonging to the loop
        boundary_loops = djs.group

        # in order to order the returned nodes again, their corresponding edges
        # have to be found first
        new_loops = dict()
        for i, loop in enumerate(boundary_loops):

            l_edges = list()
            for node in boundary_loops[loop]:
                l_edges += [sorted(edge) for edge in edges if node in edge]

            # remove duplicate list elements from l_edges
            loop_edges = [k for k, _ in itertools.groupby(sorted(l_edges))]
            new_loops[i] = loop_edges

        # new_loops[0] is airfoil
        # new_loops[1] is complete outer boundary
        # split outer boundary into inlet and outlet
        self.is_outlet = list()

        # edge is e.g.: (27, 53)
        for i, edge in enumerate(new_loops[1]):
            self.is_outlet.append(0)

            # vertices[edge[0]] is e.g.: (1.0453006577029285, 3.5)
            vector = Utils.vector(vertices[edge[0]], vertices[edge[1]])
            # check angle against y-axis
            angle = Utils.angle_between(vector, (0., 1.), degree=True)

            # FIXME
            # FIXME find better criterions or at leat refactor
            # FIXME
            # angle tolerance
            tol = 0.5
            # check only for edges downstream the airfoil
            tol_wake = 1.1
            if ((angle > - tol) and (angle < tol)) or \
                    ((angle > 180. - tol) and (angle < 180. + tol)) and \
                    vertices[edge[0]][0] > tol_wake:
                self.is_outlet[i] = 1

        return new_loops

    def drawMesh(self, airfoil):
        """Add the mesh as ItemGroup to the scene

        Args:
            airfoil (TYPE): object containing all airfoil properties and data
        """

        # toggle spline points
        self.mainwindow.centralwidget.cb3.click()

        # delete old mesh if existing
        if hasattr(airfoil, 'mesh'):
            logger.debug('MESH item type: {}'.format(type(airfoil.mesh)))
            self.mainwindow.scene.removeItem(airfoil.mesh)

        mesh = list()

        for block in self.blocks:
            for lines in [block.getULines(),
                          block.getVLines()]:
                for line in lines:

                    # instantiate a graphics item
                    contour = gic.GraphicsCollection()
                    # make it polygon type and populate its points
                    points = [QtCore.QPointF(x, y) for x, y in line]
                    contour.Polyline(QtGui.QPolygonF(points), '')
                    # set its properties
                    contour.pen.setColor(QtGui.QColor(0, 0, 0, 255))
                    contour.pen.setWidthF(0.8)
                    contour.pen.setCosmetic(True)
                    contour.brush.setStyle(QtCore.Qt.NoBrush)

                    # add contour as a GraphicsItem to the scene
                    # these are the objects which are drawn in the GraphicsView
                    meshline = GraphicsItem.GraphicsItem(contour)
                    mesh.append(meshline)

        airfoil.mesh = self.mainwindow.scene.createItemGroup(mesh)

        # activate viewing options if mesh is created and displayed
        self.mainwindow.centralwidget.cb6.setChecked(True)
        self.mainwindow.centralwidget.cb6.setEnabled(True)

    def drawBlockOutline(self, airfoil):
        """Add the mesh block outlines to the scene

        Args:
            airfoil (TYPE): object containing all airfoil properties and data
        """

        # FIXME
        # FIXME Refactroing of code duplication here and in drawMesh
        # FIXME

        mesh_blocks = list()

        for block in self.blocks:
            for lines in [block.getULines()]:
                for line in [lines[0], lines[-1]]:

                    # instantiate a graphics item
                    contour = gic.GraphicsCollection()
                    # make it polygon type and populate its points
                    points = [QtCore.QPointF(x, y) for x, y in line]
                    contour.Polyline(QtGui.QPolygonF(points), '')
                    # set its properties
                    contour.pen.setColor(QtGui.QColor(202, 31, 123, 255))
                    contour.pen.setWidthF(3.0)
                    contour.pen.setCosmetic(True)
                    contour.brush.setStyle(QtCore.Qt.NoBrush)

                    # add contour as a GraphicsItem to the scene
                    # these are the objects which are drawn in the GraphicsView
                    meshline = GraphicsItem.GraphicsItem(contour)
                    mesh_blocks.append(meshline)

            for lines in [block.getVLines()]:
                for line in [lines[0], lines[-1]]:

                    # instantiate a graphics item
                    contour = gic.GraphicsCollection()
                    # make it polygon type and populate its points
                    points = [QtCore.QPointF(x, y) for x, y in line]
                    contour.Polyline(QtGui.QPolygonF(points), '')
                    # set its properties
                    contour.pen.setColor(QtGui.QColor(202, 31, 123, 255))
                    contour.pen.setWidthF(3.0)
                    contour.pen.setCosmetic(True)
                    contour.brush.setStyle(QtCore.Qt.NoBrush)

                    # add contour as a GraphicsItem to the scene
                    # these are the objects which are drawn in the GraphicsView
                    meshline = GraphicsItem.GraphicsItem(contour)
                    mesh_blocks.append(meshline)

        airfoil.mesh_blocks = self.mainwindow.scene \
            .createItemGroup(mesh_blocks)

        # activate viewing options if mesh is created and displayed
        self.mainwindow.centralwidget.cb8.setChecked(True)
        self.mainwindow.centralwidget.cb8.setEnabled(True)
        # after instantiating everything above switch it off
        # as blocks should not be shown as a default
        # now visibility of blocks fits to checkbox setting
        self.mainwindow.centralwidget.cb8.click()


class BlockMesh:

    def __init__(self, name='block'):
        self.name = name
        self.ULines = list()

    def addLine(self, line):
        # line is a list of (x, y) tuples
        self.ULines.append(line)

    def getULines(self):
        return self.ULines

    def getVLines(self):
        vlines = list()
        U, V = self.getDivUV()

        # loop over all u-lines
        for i in range(U + 1):
            # prepare new v-line
            vline = list()
            # collect i-th point on each u-line
            for uline in self.getULines():
                vline.append(uline[i])
            vlines.append(vline)

        return vlines

    def getLine(self, number=0, direction='u'):
        if direction.lower() == 'u':
            lines = self.getULines()
        if direction.lower() == 'v':
            lines = self.getVLines()
        return lines[number]

    def getDivUV(self):
        u = len(self.getULines()[0]) - 1
        v = len(self.getULines()) - 1
        return u, v

    def getNodeCoo(self, node):
        I, J = node[0], node[1]
        uline = self.getULines()[J]
        point = uline[I]
        return np.array(point)

    def setNodeCoo(self, node, new_pos):
        I, J = node[0], node[1]
        uline = self.getULines()[J]
        uline[I] = new_pos
        return

    @staticmethod
    def makeLine(p1, p2, divisions=1, ratio=1.0):
        vec = p2 - p1
        dist = np.linalg.norm(vec)
        spacing = BlockMesh.spacing(divisions=divisions,
                                    ratio=ratio, length=dist)
        line = list()
        line.append((p1.tolist()[0], p1.tolist()[1]))
        for i in range(1, len(spacing)):
            p = p1 + spacing[i] * Utils.unit_vector(vec)
            line.append((p.tolist()[0], p.tolist()[1]))
        del line[-1]
        line.append((p2.tolist()[0], p2.tolist()[1]))
        return line

    def extrudeLine_cell_thickness(self, line, cell_thickness=0.04,
                                   growth=1.05,
                                   divisions=1,
                                   direction=3):
        x, y = list(zip(*line))
        x = np.array(x)
        y = np.array(y)
        if direction == 3:
            spacing, _ = self.spacing_cell_thickness(
                cell_thickness=cell_thickness,
                growth=growth,
                divisions=divisions)
            normals = self.curveNormals(x, y)
            for i in range(1, len(spacing)):
                xo = x + spacing[i] * normals[:, 0]
                yo = y + spacing[i] * normals[:, 1]
                line = list(zip(xo.tolist(), yo.tolist()))
                self.addLine(line)
        elif direction == 4:
            spacing, _ = self.spacing_cell_thickness(
                cell_thickness=cell_thickness,
                growth=growth,
                divisions=divisions)
            normals = self.curveNormals(x, y)
            normalx = normals[:, 0].mean()
            normaly = normals[:, 1].mean()
            for i in range(1, len(spacing)):
                xo = x + spacing[i] * normalx
                yo = y + spacing[i] * normaly
                line = list(zip(xo.tolist(), yo.tolist()))
                self.addLine(line)

    def extrudeLine(self, line, direction=0, length=0.1, divisions=1,
                    ratio=1.00001, constant=False):
        x, y = list(zip(*line))
        x = np.array(x)
        y = np.array(y)
        if constant and direction == 0:
            x.fill(length)
            line = list(zip(x.tolist(), y.tolist()))
            self.addLine(line)
        elif constant and direction == 1:
            y.fill(length)
            line = list(zip(x.tolist(), y.tolist()))
            self.addLine(line)
        elif direction == 3:
            spacing = self.spacing(divisions=divisions,
                                   ratio=ratio,
                                   length=length)
            normals = self.curveNormals(x, y)
            for i in range(1, len(spacing)):
                xo = x + spacing[i] * normals[:, 0]
                yo = y + spacing[i] * normals[:, 1]
                line = list(zip(xo.tolist(), yo.tolist()))
                self.addLine(line)
        elif direction == 4:
            spacing = self.spacing(divisions=divisions,
                                   ratio=ratio,
                                   length=length)
            normals = self.curveNormals(x, y)
            normalx = normals[:, 0].mean()
            normaly = normals[:, 1].mean()
            for i in range(1, len(spacing)):
                xo = x + spacing[i] * normalx
                yo = y + spacing[i] * normaly
                line = list(zip(xo.tolist(), yo.tolist()))
                self.addLine(line)

    def distribute(self, direction='u', number=0, type='constant'):

        if direction == 'u':
            line = np.array(self.getULines()[number])
        elif direction == 'v':
            line = np.array(self.getVLines()[number])

        # interpolate B-spline through data points
        # here, a linear interpolant is derived "k=1"
        # splprep returns:
        # tck ... tuple (t,c,k) containing the vector of knots,
        #         the B-spline coefficients, and the degree of the spline.
        #   u ... array of the parameters for each given point (knot)
        tck, u = interpolate.splprep(line.T, s=0, k=1)

        if type == 'constant':
            t = np.linspace(0.0, 1.0, num=len(line))
        if type == 'transition':
            first = np.array(self.getULines()[0])
            last = np.array(self.getULines()[-1])
            tck_first, u_first = interpolate.splprep(first.T, s=0, k=1)
            tck_last, u_last = interpolate.splprep(last.T, s=0, k=1)
            if number < 0.0:
                number = len(self.getVLines())
            v = float(number) / float(len(self.getVLines()))
            t = (1.0 - v) * u_first + v * u_last

        # evaluate function at any parameter "0<=t<=1"
        line = interpolate.splev(t, tck, der=0)
        line = list(zip(line[0].tolist(), line[1].tolist()))

        if direction == 'u':
            self.getULines()[number] = line
        elif direction == 'v':
            for i, uline in enumerate(self.getULines()):
                self.getULines()[i][number] = line[i]

    @staticmethod
    def spacing_cell_thickness(cell_thickness=0.04, growth=1.1, divisions=10):

        # add cell thickness of first layer
        spacing = [cell_thickness]

        for i in range(divisions - 1):
            spacing.append(spacing[0] + spacing[-1] * growth)

        spacing.insert(0, 0.0)

        length = np.sum(spacing)

        return spacing, length

    @staticmethod
    def spacing(divisions=10, ratio=1.0, length=1.0):
        """Calculate point distribution on a line

        Args:
            divisions (int, optional): Number of subdivisions
            ratio (float, optional): Ratio of last to first subdivision size
            length (float, optional): length of line

        Returns:
            array: individual line segment lengths
        """

        if divisions == 1:
            sp = [0.0, 1.0]
            return np.array(sp)

        growth = ratio**(1.0 / (float(divisions) - 1.0))

        if growth == 1.0:
            growth = 1.0 + 1.0e-10

        s = [1.0]
        for i in range(1, divisions + 1):
            s.append(growth**i)

        spacing = np.array(s)
        spacing -= spacing[0]
        spacing /= spacing[-1]
        spacing *= length

        return spacing

    def mapLines(self, line_1, line_2):
        """Map the distribution of points from one line to another line

        Args:
            line_1 (LIST): Source line (will be mapped)
            line_2 (LIST): Destination line (upon this line_1 is mapped)
        """
        pass

    @staticmethod
    def curveNormals(x, y, closed=False):
        istart = 0
        iend = 0
        n = list()

        for i, _ in enumerate(x):

            if closed:
                if i == len(x) - 1:
                    iend = -i - 1
            else:
                if i == 0:
                    istart = 1
                if i == len(x) - 1:
                    iend = -1

            a = np.array([x[i + 1 + iend] - x[i - 1 + istart],
                          y[i + 1 + iend] - y[i - 1 + istart]])
            e = Utils.unit_vector(a)
            n.append([e[1], -e[0]])
            istart = 0
            iend = 0
        return np.array(n)

    def transfinite(self, boundary=[], ij=[]):
        """Make a transfinite interpolation.

        http://en.wikipedia.org/wiki/Transfinite_interpolation

                       upper
                --------------------
                |                  |
                |                  |
           left |                  | right
                |                  |
                |                  |
                --------------------
                       lower

        Example input for the lower boundary:
            lower = [(0.0, 0.0), (0.1, 0.3),  (0.5, 0.4)]
        """

        if boundary:
            lower = boundary[0]
            upper = boundary[1]
            left = boundary[2]
            right = boundary[3]
        elif ij:
            lower = self.getULines()[ij[2]][ij[0]:ij[1] + 1]
            upper = self.getULines()[ij[3]][ij[0]:ij[1] + 1]
            left = self.getVLines()[ij[0]][ij[2]:ij[3] + 1]
            right = self.getVLines()[ij[1]][ij[2]:ij[3] + 1]
        else:
            lower = self.getULines()[0]
            upper = self.getULines()[-1]
            left = self.getVLines()[0]
            right = self.getVLines()[-1]

        # FIXME
        # FIXME left and right need to swapped from input
        # FIXME
        # FIXME like: left, right = right, left
        # FIXME

        lower = np.array(lower)
        upper = np.array(upper)
        left = np.array(left)
        right = np.array(right)

        # convert the block boundary curves into parametric form
        # as curves need to be between 0 and 1
        # interpolate B-spline through data points
        # here, a linear interpolant is derived "k=1"
        # splprep returns:
        # tck ... tuple (t,c,k) containing the vector of knots,
        #         the B-spline coefficients, and the degree of the spline.
        #   u ... array of the parameters for each given point (knot)
        tck_lower, u_lower = interpolate.splprep(lower.T, s=0, k=1)
        tck_upper, u_upper = interpolate.splprep(upper.T, s=0, k=1)
        tck_left, u_left = interpolate.splprep(left.T, s=0, k=1)
        tck_right, u_right = interpolate.splprep(right.T, s=0, k=1)

        nodes = np.zeros((len(left) * len(lower), 2))

        # corner points
        c1 = lower[0]
        c2 = upper[0]
        c3 = lower[-1]
        c4 = upper[-1]

        for i, xi in enumerate(u_lower):
            for j, eta in enumerate(u_left):

                node = i * len(u_left) + j

                point = (1.0 - xi) * left[j] + xi * right[j] + \
                    (1.0 - eta) * lower[i] + eta * upper[i] - \
                    ((1.0 - xi) * (1.0 - eta) * c1 + (1.0 - xi) * eta * c2 +
                     xi * (1.0 - eta) * c3 + xi * eta * c4)

                nodes[node, 0] = point[0]
                nodes[node, 1] = point[1]

        vlines = list()
        vline = list()
        i = 0
        for node in nodes:
            i += 1
            vline.append(node)
            if i % len(left) == 0:
                vlines.append(vline)
                vline = list()

        vlines.reverse()

        if ij:
            ulines = self.makeUfromV(vlines)
            n = -1
            for k in range(ij[2], ij[3] + 1):
                n += 1
                self.ULines[k][ij[0]:ij[1] + 1] = ulines[n]
        else:
            self.ULines = self.makeUfromV(vlines)

        return

    @staticmethod
    def makeUfromV(vlines):
        ulines = list()
        uline = list()
        for i in range(len(vlines[0])):
            for vline in vlines:
                x, y = vline[i][0], vline[i][1]
                uline.append((x, y))
            ulines.append(uline[::-1])
            uline = list()
        return ulines

    @staticmethod
    def writeFLMA(wind_tunnel, name='', depth=0.3):

        basename = os.path.basename(name)
        nameroot, extension = os.path.splitext(basename)

        mesh = wind_tunnel.mesh

        vertices, connectivity = mesh

        with open(name, 'w') as f:

            number_of_vertices_2D = len(vertices)

            numvertex = '8'

            # write number of points to FLMA file (*2 for z-direction)
            f.write(str(2 * number_of_vertices_2D) + '\n')

            signum = -1.

            # write x-, y- and z-coordinates to FLMA file
            # loop 1D direction (symmetry)
            for _ in range(2):
                for vertex in vertices:
                    f.write(str(vertex[0]) + ' ' + str(vertex[1]) +
                            ' ' + str(signum * depth / 2.0) + ' ')
                signum = 1.

            # write number of cells to FLMA file
            cells = len(connectivity)
            f.write('\n' + str(cells) + '\n')

            # write cell connectivity to FLMA file
            for cell in connectivity:
                cell_connect = str(cell[0]) + ' ' + \
                    str(cell[1]) + ' ' + \
                    str(cell[2]) + ' ' + \
                    str(cell[3]) + ' ' + \
                    str(cell[0] + number_of_vertices_2D) + ' ' + \
                    str(cell[1] + number_of_vertices_2D) + ' ' + \
                    str(cell[2] + number_of_vertices_2D) + ' ' + \
                    str(cell[3] + number_of_vertices_2D) + '\n'

                f.write(numvertex + '\n')
                f.write(cell_connect)

            # FIRE element type (FET) for HEX element
            fetHEX = '5'
            f.write('\n' + str(cells) + '\n')
            for i in range(cells):
                f.write(fetHEX + ' ')
            f.write('\n\n')

            # FIRE element type (FET) for Quad element
            fetQuad = '3\n'

            # write FIRE selections to FLMA file
            f.write('6\n')
            f.write('right\n')
            f.write(fetQuad)
            f.write(str(2 * len(connectivity)) + '\n')
            for i in range(len(connectivity)):
                f.write(' %s 0' % (i))
            f.write('\n')
            f.write('\n')
            f.write('left\n')
            f.write(fetQuad)
            f.write(str(2 * len(connectivity)) + '\n')
            for i in range(len(connectivity)):
                f.write(' %s 1' % (i))
            f.write('\n')
            f.write('\n')
            f.write('bottom\n')
            f.write(fetQuad)
            f.write('2\n')
            f.write('0 2\n')
            f.write('\n')
            f.write('top\n')
            f.write(fetQuad)
            f.write('2\n')
            f.write('0 3\n')
            f.write('\n')
            f.write('back\n')
            f.write(fetQuad)
            f.write('2\n')
            f.write('0 4\n')
            f.write('\n')
            f.write('front\n')
            f.write(fetQuad)
            f.write('2\n')
            f.write('0 5\n')

            logger.info('FIRE type mesh saved as {}'.
                        format(os.path.join(OUTPUTDATA, basename)))

    @staticmethod
    def writeSU2(wind_tunnel, name=''):

        basename = os.path.basename(name)
        nameroot, extension = os.path.splitext(basename)

        mesh = wind_tunnel.mesh
        blocks = wind_tunnel.blocks
        boundary_loops = wind_tunnel.boundary_loops

        vertices, connectivity = mesh
        airfoil_subdivisions, v = blocks[0].getDivUV()
        trailing_edge_subdivisions, _ = blocks[1].getDivUV()

        # SU2 element types
        element_type_quadrilateral = '9'

        _date = date.today().strftime("%A %d. %B %Y")

        with open(name, 'w') as f:
            f.write('%\n')
            f.write('% Airfoil contour: ' + nameroot + ' \n')
            f.write('%\n')
            f.write('% File created with ' + PyAero.__appname__ + '\n')
            f.write('% Version: ' + PyAero.__version__ + '\n')
            f.write('% Author: ' + PyAero.__author__ + '\n')
            f.write('% Date: ' + _date + '\n')
            # dimension of the problem
            f.write('%\n')
            f.write('% Problem dimension\n')
            f.write('%\n')
            f.write('NDIME= 2\n')
            # element connectivity
            f.write('%\n')
            f.write('% Inner element connectivity\n')
            f.write('%\n')
            # number of elements
            f.write('NELEM= %s\n' % (len(connectivity)))

            for cell_id, cell in enumerate(connectivity):

                cell_connect = element_type_quadrilateral + ' ' + \
                    str(cell[0]) + ' ' + \
                    str(cell[1]) + ' ' + \
                    str(cell[2]) + ' ' + \
                    str(cell[3]) + ' ' + str(cell_id) + '\n'

                f.write(cell_connect)

            # comment for vertices
            f.write('%\n')
            f.write('% Node coordinates\n')
            f.write('%\n')

            f.write('NPOIN=%s\n' % (len(vertices)))

            # x- and y-coordinates
            for node, vertex in enumerate(vertices):
                x, y = vertex[0], vertex[1]
                f.write(' {:24.16e} {:24.16e} {}\n'.format(x, y, node))

            # boundaries
            f.write('%\n')
            f.write('% Boundary elements\n')
            f.write('%\n')

            # number of vertices
            # number of marks (Airfoil, Farfield, Symmetry)
            # f.write('NMARK= 3\n')
            f.write('NMARK= 2\n')

            # boundary definition (tag) for the airfoil
            f.write('MARKER_TAG= {}\n'.format(wind_tunnel.boundary_airfoil))
            f.write('MARKER_ELEMS= {}\n'.format(len(boundary_loops[0])))
            for edge in boundary_loops[0]:
                f.write('3 {} {}\n'.format(edge[0], edge[1]))

            # boundary definition (tag) for the farfield
            # this loops the complete outer boundary
            f.write('MARKER_TAG= {}\n'.format(wind_tunnel.boundary_inlet))
            f.write('MARKER_ELEMS= {}\n'.format(len(boundary_loops[1])))
            for edge in boundary_loops[1]:
                f.write('3 {} {}\n'.format(edge[0], edge[1]))

            # boundary definition (tag) for the symmetry
            # f.write('MARKER_TAG= {}\n'.format(wind_tunnel.boundary_symmetry))
            # f.write('MARKER_ELEMS= {}\n'.format(len(connectivity)))
            # for cell_id, cell in enumerate(connectivity):

            #     cell_connect = element_type_quadrilateral + ' ' + \
            #         str(cell[0]) + ' ' + \
            #         str(cell[1]) + ' ' + \
            #         str(cell[2]) + ' ' + \
            #         str(cell[3])
            #     f.write('{}\n'.format(cell_connect))

            logger.info('SU2 type mesh saved as {}'.
                        format(name))

    @staticmethod
    def writeGMSH(wind_tunnel, name=''):
        """export mesh in GMSH format 2
        http://gmsh.info/doc/texinfo/gmsh.html#MSH-file-format-version-2-_0028Legacy_0029

        Args:
            mesh (TYPE): Description
            blocks (TYPE): Description
            name (str, optional): Description
        """
        basename = os.path.basename(name)
        nameroot, extension = os.path.splitext(basename)

        mesh = wind_tunnel.mesh
        boundary_loops = wind_tunnel.boundary_loops
        bnd_airfoil = wind_tunnel.lineedit_airfoil
        bnd_inlet = wind_tunnel.lineedit_inlet
        bnd_outlet = wind_tunnel.lineedit_outlet
        bnd_symmetry = wind_tunnel.lineedit_symmetry
        is_outlet = wind_tunnel.is_outlet

        vertices, connectivity = mesh

        # element type "1" is GMSH 2-node line
        # element type "2" is GMSH 3-node triangle
        # element type "3" is GMSH 4-node quadrangle
        element_type_line = '1'
        # element_type_triangle = '2'
        element_type_quadrangle = '3'

        # write date in English
        locale.setlocale(locale.LC_ALL, 'en')
        _date = date.today().strftime("%A %d. %B %Y")

        with open(name, 'w') as f:

            f.write('$MeshFormat\n')
            f.write('2.2 0 8\n')
            f.write('$EndMeshFormat\n')
            f.write('$Comments\n')
            f.write(' Airfoil contour: ' + nameroot + ' \n')
            f.write(' File created with ' + PyAero.__appname__ + '\n')
            f.write(' Version: ' + PyAero.__version__ + '\n')
            f.write(' Author: ' + PyAero.__author__ + '\n')
            f.write(' Date: ' + _date + '\n')
            f.write('$EndComments\n')
            '''
            $PhysicalNames
            number-of-names
            physical-dimension physical-tag "physical-name"
            $EndPhysicalNames
            '''
            f.write('$PhysicalNames\n')
            f.write('4\n')
            f.write('1 1 "{}"\n'.format(bnd_airfoil))
            f.write('1 2 "{}"\n'.format(bnd_inlet))
            f.write('1 3 "{}"\n'.format(bnd_outlet))
            f.write('2 4 "{}"\n'.format(bnd_symmetry))
            f.write('$EndPhysicalNames\n')
            f.write('$Nodes\n')
            f.write('%s\n' % (len(vertices)))

            # x- and y-coordinates
            for node, vertex in enumerate(vertices, start=1):
                x, y = vertex[0], vertex[1]
                f.write(' {:} {:16.8} {:16.8} 0.0\n'.format(node, x, y))
            f.write('$EndNodes\n')
            '''
            $Elements
            number-of-elements
            elm-number elm-type number-of-tags < tag > … node-number-list
            $EndElements
            '''
            f.write('$Elements\n')

            # boundary_loops is a disjoint set groups element
            # key for each loop is one arbitrary vertex of the loop
            # one loop is made by the airfoil
            # the other loop is made by the windtunnel outer boundary
            keys = list(boundary_loops.keys())
            # print('Number of boundary loops', len(keys))
            elements_loop1 = len(list(boundary_loops[keys[0]]))
            elements_loop2 = len(list(boundary_loops[keys[1]]))
            number_of_cells = len(connectivity)

            # number of elements
            # compiled of airfoil, outer boundary and mesh itself
            f.write('{}\n'.format(elements_loop1 + elements_loop2 +
                                  number_of_cells))

            element_id = 0

            # FIXME
            # FIXME refactor dicts and their usage
            # FIXME
            # write boundary elements (as per physical names)
            physical = {0: '1', 1: '2'}
            elementary_entities = {0: '8', 1: '7'}
            for j, loop in enumerate(boundary_loops):
                for i, node in enumerate(boundary_loops[loop]):
                    element_id += 1
                    # an element consists of:
                    #   element_id
                    #   element_type
                    #
                    if is_outlet[i]:
                        physical_l = '3'
                        elementary_entities_l = '9'
                    else:
                        physical_l = physical[j]
                        elementary_entities_l = elementary_entities[j]
                    element = ' ' + str(element_id) + ' ' + \
                        element_type_line + ' 3 ' + physical_l + ' ' + \
                        elementary_entities_l + ' 0 ' + str(node[0] + 1) + \
                        ' ' + str(node[1] + 1) + '\n'
                    f.write(element)

            # write mesh elements
            # includes physical tag for symmetry "4"
            for cell in connectivity:

                element_id += 1
                element = ' ' + str(element_id) + ' ' + \
                    element_type_quadrangle + ' 3 4 6 0 ' + \
                    str(cell[0] + 1) + ' ' + \
                    str(cell[1] + 1) + ' ' + \
                    str(cell[2] + 1) + ' ' + \
                    str(cell[3] + 1) + '\n'

                f.write(element)

            f.write('$EndElements')

            logger.info('GMSH type mesh saved as {}'.
                        format(os.path.join(OUTPUTDATA, basename)))


class Smooth:

    def __init__(self, block):
        self.block = block

    def getNeighbours(self, node):
        """Get a list of neighbours around a node """

        i, j = node[0], node[1]
        neighbours = {1: (i - 1, j - 1), 2: (i, j - 1), 3: (i + 1, j - 1),
                      4: (i + 1, j), 5: (i + 1, j + 1), 6: (i, j + 1),
                      7: (i - 1, j + 1), 8: (i - 1, j)}
        return neighbours

    def smooth(self, nodes, iterations=1, algorithm='laplace'):
        """Smoothing of a square lattice mesh

        Algorithms:
           - Angle based
             Tian Zhou:
             AN ANGLE-BASED APPROACH TO TWO-DIMENSIONAL MESH SMOOTHING
           - Laplace
             Mean of surrounding node coordinates
           - Parallelogram smoothing
             Sanjay Kumar Khattri:
             A NEW SMOOTHING ALGORITHM FOR QUADRILATERAL AND HEXAHEDRAL MESHES

        Args:
            nodes (TYPE): List of (i, j) tuples for the nodes to be smoothed
            iterations (int, optional): Number of smoothing iterations
            algorithm (str, optional): Smoothing algorithm
        """

        # loop number of smoothing iterations
        for i in range(iterations):

            new_pos = list()

            # smooth a node (i, j)
            for node in nodes:
                nb = self.getNeighbours(node)

                if algorithm == 'laplace':
                    new_pos = (self.block.getNodeCoo(nb[2]) +
                               self.block.getNodeCoo(nb[4]) +
                               self.block.getNodeCoo(nb[6]) +
                               self.block.getNodeCoo(nb[8])) / 4.0

                if algorithm == 'parallelogram':

                    new_pos = (self.block.getNodeCoo(nb[1]) +
                               self.block.getNodeCoo(nb[3]) +
                               self.block.getNodeCoo(nb[5]) +
                               self.block.getNodeCoo(nb[7])) / 4.0 - \
                              (self.block.getNodeCoo(nb[2]) +
                               self.block.getNodeCoo(nb[4]) +
                               self.block.getNodeCoo(nb[6]) +
                               self.block.getNodeCoo(nb[8])) / 2.0

                if algorithm == 'angle_based':
                    pass

                self.block.setNodeCoo(node, new_pos.tolist())

        return self.block

    def selectNodes(self, domain='interior', ij=[]):
        """Generate a node index list

        Args:
            domain (str, optional): Defines the part of the domain where
                                    nodes shall be selected

        Returns:
            List: Indices as (i, j) tuples
        """
        U, V = self.block.getDivUV()
        nodes = list()

        # select all nodes except boundary nodes
        if domain == 'interior':
            istart = 1
            iend = U
            jstart = 1
            jend = V

        if domain == 'ij':
            istart = ij[0]
            iend = ij[1]
            jstart = ij[2]
            jend = ij[3]

        for i in range(istart, iend):
            for j in range(jstart, jend):
                nodes.append((i, j))

        return nodes


class DisjointSet:
    """Summary

    Attributes:
        group (dict): Description
        leader (dict): Description
        oldgroup (dict): Description
        oldleader (dict): Description

    from: https://stackoverflow.com/a/3067672/2264936
    """

    def __init__(self, size=None):
        if size is None:
            # maps a member to the group's leader
            self.leader = {}
            # maps a group leader to the group (which is a set)
            self.group = {}
            self.oldgroup = {}
            self.oldleader = {}
        else:
            self.group = {i: set([i]) for i in range(0, size)}
            self.leader = {i: i for i in range(0, size)}
            self.oldgroup = {i: set([i]) for i in range(0, size)}
            self.oldleader = {i: i for i in range(0, size)}

    def add(self, a, b):
        self.oldgroup = self.group.copy()
        self.oldleader = self.leader.copy()
        leadera = self.leader.get(a)
        leaderb = self.leader.get(b)
        if leadera is not None:
            if leaderb is not None:
                if leadera == leaderb:
                    return  # nothing to do
                groupa = self.group[leadera]
                groupb = self.group[leaderb]
                if len(groupa) < len(groupb):
                    a, leadera, groupa, b, leaderb, groupb = \
                        b, leaderb, groupb, a, leadera, groupa
                groupa |= groupb
                del self.group[leaderb]
                for k in groupb:
                    self.leader[k] = leadera
            else:
                self.group[leadera].add(b)
                self.leader[b] = leadera
        else:
            if leaderb is not None:
                self.group[leaderb].add(a)
                self.leader[a] = leaderb
            else:
                self.leader[a] = self.leader[b] = a
                self.group[a] = set([a, b])

    def connected(self, a, b):
        leadera = self.leader.get(a)
        leaderb = self.leader.get(b)
        if leadera is not None:
            if leaderb is not None:
                return leadera == leaderb
            else:
                return False
        else:
            return False

    def undo(self):
        self.group = self.oldgroup.copy()
        self.leader = self.oldleader.copy()