""" MeshLabXML measurement and computation functions """

#from . import mlx.FilterScript
import meshlabxml as mlx
import re
from . import util

def section(script, axis='z', offset=0.0, surface=False, custom_axis=None,
            planeref=2):
    """ Compute the polyline representing a planar section (a slice) of a mesh.

    If the resulting polyline is closed the result can be filled with a
    triangular mesh representing the section.

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.
        axis (str): The slicing plane is perpendicular to this axis. Accepted
            values are 'x', 'y', or 'z'; any other input will be interpreted
            as a custom axis (although using 'custom' is recommended
            for clarity). Upper or lowercase values are accepted.
        offset (float): Specify an offset of the cross-plane. The offset
            corresponds to the distance along 'axis' from the point specified
            in 'planeref'.
        surface (bool): If True, in addition to a layer with the section
            polyline, also a layer with a triangulated version of the section
            polyline will be created. This only works if the section polyline
            is closed.
        custom_axis (3 component list or tuple): Specify a custom axis as
            a 3 component vector (x, y, z); this is ignored unless 'axis' is
            set  to 'custom'.
        planeref (int): Specify the reference from which the planes are
            shifted. Valid values are:
            0 - Bounding box center
            1 - Bounding box min
            2 - Origin (default)

    Layer stack:
        Creates a new layer '{label}_sect_{axis_name}_{offset}', where
            'axis_name' is one of [X, Y, Z, custom] and 'offest' is
            truncated 'offset'
        If surface is True, create a new layer '{label}_sect_{axis}_{offset}_mesh'
        Current layer is changed to the last (newly created) layer

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    # Convert axis name into number
    if axis.lower() == 'x':
        axis_num = 0
        axis_name = 'X'
    elif axis.lower() == 'y':
        axis_num = 1
        axis_name = 'Y'
    elif axis.lower() == 'z':
        axis_num = 2
        axis_name = 'Z'
    else:  # custom axis
        axis_num = 3
        axis_name = 'custom'
        if custom_axis is None:
            print('WARNING: a custom axis was selected, however',
                  '"custom_axis" was not provided. Using default (Z).')
    if custom_axis is None:
        custom_axis = (0.0, 0.0, 1.0)

    filter_xml = ''.join([
        '  <filter name="Compute Planar Section">\n',
        '    <Param name="planeAxis" ',
        'value="{:d}" '.format(axis_num),
        'description="Plane perpendicular to" ',
        'enum_val0="X Axis" ',
        'enum_val1="Y Axis" ',
        'enum_val2="Z Axis" ',
        'enum_val3="Custom Axis" ',
        'enum_cardinality="4" ',
        'type="RichEnum" ',
        '/>\n',
        '    <Param name="customAxis" ',
        'x="{}" y="{}" z="{}" '.format(custom_axis[0], custom_axis[1],
                                       custom_axis[2]),
        'description="Custom axis" ',
        'type="RichPoint3f" ',
        '/>\n',
        '    <Param name="planeOffset" ',
        'value="{}" '.format(offset),
        'description="Cross plane offset" ',
        'type="RichFloat" ',
        '/>\n',
        '    <Param name="relativeTo" ',
        'value="{:d}" '.format(planeref),
        'description="plane reference" ',
        'enum_val0="Bounding box center" ',
        'enum_val1="Bounding box min" ',
        'enum_val2="Origin" ',
        'enum_cardinality="3" ',
        'type="RichEnum" ',
        '/>\n',
        '    <Param name="createSectionSurface" ',
        'value="{}" '.format(str(surface).lower()),
        'description="Create also section surface" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, mlx.FilterScript):
        current_layer_label = script.layer_stack[script.current_layer()]
        script.add_layer('{}_sect_{}_{}'.format(current_layer_label, axis_name,
                                                int(offset)))
        if surface:
            script.add_layer('{}_sect_{}_{}_mesh'.format(current_layer_label,
                                                         axis_name, int(offset)))
    return None

def measure_geometry(script):
    """ Compute a set of geometric measures of a mesh/pointcloud.

    Bounding box extents and diagonal, principal axis, thin shell barycenter
    (mesh only), vertex barycenter and quality-weighted barycenter (pointcloud
    only), surface area (mesh only), volume (closed mesh) and Inertia tensor
    Matrix (closed mesh).

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA

    Bugs:
        Bounding box extents not computed correctly for some volumes
    """
    filter_xml = '  <xmlfilter name="Compute Geometric Measures"/>\n'
    util.write_filter(script, filter_xml)
    if isinstance(script, mlx.FilterScript):
        script.parse_geometry = True
    return None

def measure_topology(script):
    """ Compute a set of topological measures over a mesh

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = '  <xmlfilter name="Compute Topological Measures"/>\n'
    util.write_filter(script, filter_xml)
    if isinstance(script, mlx.FilterScript):
        script.parse_topology = True
    return None

def parse_geometry(ml_log, log=None, ml_version='2016.12', print_output=False):
    """Parse the ml_log file generated by the measure_geometry function.

    Warnings: Not all keys may exist if mesh is not watertight or manifold

    Args:
        ml_log (str): MeshLab log file to parse
        log (str): filename to log output
    """
    # TODO: read more than one occurrence per file. Record in list.
    aabb = {}
    geometry = {'aabb':aabb}
    with open(ml_log) as fread:
        for line in fread:
            if 'Mesh Bounding Box min' in line: #2016.12
                geometry['aabb']['min'] = (line.split()[4:7])
                geometry['aabb']['min'] = [util.to_float(val) for val in geometry['aabb']['min']]
            if 'Mesh Bounding Box max' in line: #2016.12
                geometry['aabb']['max'] = (line.split()[4:7])
                geometry['aabb']['max'] = [util.to_float(val) for val in geometry['aabb']['max']]
            if 'Mesh Bounding Box Size' in line: #2016.12
                geometry['aabb']['size'] = (line.split()[4:7])
                geometry['aabb']['size'] = [util.to_float(val) for val in geometry['aabb']['size']]
            if 'Mesh Bounding Box Diag' in line: #2016.12
                geometry['aabb']['diagonal'] = util.to_float(line.split()[4])
            if 'Mesh Volume' in line:
                geometry['volume_mm3'] = util.to_float(line.split()[3])
                geometry['volume_cm3'] = geometry['volume_mm3'] * 0.001
            if 'Mesh Surface' in line:
                if ml_version == '1.3.4BETA':
                    geometry['area_mm2'] = util.to_float(line.split()[3])
                else:
                    geometry['area_mm2'] = util.to_float(line.split()[4])
                geometry['area_cm2'] = geometry['area_mm2'] * 0.01
            if 'Mesh Total Len of' in line:
                if 'including faux edges' in line:
                    geometry['total_edge_length_incl_faux'] = util.to_float(
                        line.split()[7])
                else:
                    geometry['total_edge_length'] = util.to_float(
                        line.split()[7])
            if 'Thin shell barycenter' in line:
                geometry['barycenter'] = (line.split()[3:6])
                geometry['barycenter'] = [util.to_float(val) for val in geometry['barycenter']]
            if 'Thin shell (faces) barycenter' in line: #2016.12
                geometry['barycenter'] = (line.split()[4:7])
                geometry['barycenter'] = [util.to_float(val) for val in geometry['barycenter']]
            if 'Vertices barycenter' in line: #2016.12
                geometry['vert_barycenter'] = (line.split()[2:5])
                geometry['vert_barycenter'] = [util.to_float(val) for val in geometry['vert_barycenter']]
            if 'Center of Mass' in line:
                geometry['center_of_mass'] = (line.split()[4:7])
                geometry['center_of_mass'] = [util.to_float(val) for val in geometry['center_of_mass']]
            if 'Inertia Tensor' in line:
                geometry['inertia_tensor'] = []
                for val in range(3):
                    row = (next(fread, val).split()[1:4])
                    row = [util.to_float(b) for b in row]
                    geometry['inertia_tensor'].append(row)
            if 'Principal axes' in line:
                geometry['principal_axes'] = []
                for val in range(3):
                    row = (next(fread, val).split()[1:4])
                    row = [util.to_float(b) for b in row]
                    geometry['principal_axes'].append(row)
            if 'axis momenta' in line:
                geometry['axis_momenta'] = (next(fread).split()[1:4])
                geometry['axis_momenta'] = [util.to_float(val) for val in geometry['axis_momenta']]
                break  # stop after we find the first match
    for key, value in geometry.items():
        if log is not None:
            log_file = open(log, 'a')
            log_file.write('{:27} = {}\n'.format(key, value))
            log_file.close()
        elif print_output:
            print('{:27} = {}'.format(key, value))
    return geometry

def parse_topology(ml_log, log=None, ml_version='1.3.4BETA', print_output=False):
    """Parse the ml_log file generated by the measure_topology function.

    Args:
        ml_log (str): MeshLab log file to parse
        log (str): filename to log output

    Returns:
        dict: dictionary with the following keys:
            vert_num (int): number of vertices
            edge_num (int): number of edges
            face_num (int): number of faces
            unref_vert_num (int): number or unreferenced vertices
            boundry_edge_num (int): number of boundary edges
            part_num (int): number of parts (components) in the mesh.
            manifold (bool): True if mesh is two-manifold, otherwise false.
            non_manifold_edge (int): number of non_manifold edges.
            non_manifold_vert (int): number of non-manifold verices
            genus (int or str): genus of the mesh, either a number or
                'undefined' if the mesh is non-manifold.
            holes (int or str): number of holes in the mesh, either a number
                or 'undefined' if the mesh is non-manifold.

    """
    topology = {'manifold': True, 'non_manifold_E': 0, 'non_manifold_V': 0}
    with open(ml_log) as fread:
        for line in fread:
            if 'V:' in line:
                vert_edge_face = line.replace('V:', ' ').replace('E:', ' ').replace('F:', ' ').split()
                topology['vert_num'] = int(vert_edge_face[0])
                topology['edge_num'] = int(vert_edge_face[1])
                topology['face_num'] = int(vert_edge_face[2])
            if 'Unreferenced Vertices' in line:
                topology['unref_vert_num'] = int(line.split()[2])
            if 'Boundary Edges' in line:
                topology['boundry_edge_num'] = int(line.split()[2])
            if 'Mesh is composed by' in line:
                topology['part_num'] = int(line.split()[4])
            if 'non 2-manifold mesh' in line:
                topology['manifold'] = False
            if 'non two manifold edges' in line:
                topology['non_manifold_edge'] = int(line.split()[2])
            if 'non two manifold vertexes' in line:
                topology['non_manifold_vert'] = int(line.split()[2])
            if 'Genus is' in line:  # undefined or int
                topology['genus'] = line.split()[2]
                if topology['genus'] != 'undefined':
                    topology['genus'] = int(topology['genus'])
            if 'holes' in line:
                topology['hole_num'] = line.split()[2]
                if topology['hole_num'] == 'a':
                    topology['hole_num'] = 'undefined'
                else:
                    topology['hole_num'] = int(topology['hole_num'])
    for key, value in topology.items():
        if log is not None:
            log_file = open(log, 'a')
            log_file.write('{:16} = {}\n'.format(key, value))
            log_file.close()
        elif print_output:
            print('{:16} = {}'.format(key, value))

    return topology

def parse_hausdorff(ml_log, log=None, print_output=False):
    """Parse the ml_log file generated by the hausdorff_distance function.

    Args:
        ml_log (str): MeshLab log file to parse
        log (str): filename to log output

    Returns:
        dict: dictionary with the following keys:
            number_points (int): number of points in mesh
            min_distance (float): minimum hausdorff distance
            max_distance (float): maximum hausdorff distance
            mean_distance (float): mean hausdorff distance
            rms_distance (float): root mean square distance

    """
    hausdorff_distance = {"min_distance": 0.0,
                          "max_distance": 0.0,
                          "mean_distance": 0.0,
                          "rms_distance": 0.0,
                          "number_points": 0}
    with open(ml_log) as fread:
        result = fread.readlines()
        data = ""

        for idx, line in enumerate(result):
            m = re.match(r"\s*Sampled (\d+) pts.*", line)
            if m is not None:
                hausdorff_distance["number_points"] = int(m.group(1))
            if 'Hausdorff Distance computed' in line:
                data = result[idx + 2]

        m = re.match(r"\D+(\d+\.*\d*)\D+(\d+\.*\d*)\D+(\d+\.*\d*)\D+(\d+\.*\d*)", data)
        hausdorff_distance["min_distance"] = float(m.group(1))
        hausdorff_distance["max_distance"] = float(m.group(2))
        hausdorff_distance["mean_distance"] = float(m.group(3))
        hausdorff_distance["rms_distance"] = float(m.group(4))
        for key, value in hausdorff_distance.items():
            if log is not None:
                log_file = open(log, 'a')
                log_file.write('{:16} = {}\n'.format(key, value))
                log_file.close()
            elif print_output:
                print('{:16} = {}'.format(key, value))
        return hausdorff_distance


""" MeshLabXML vertex color functions """

import math

from . import util
from .color_names import color_name

def function(script, red=255, green=255, blue=255, alpha=255, color=None):
    """Color function using muparser lib to generate new RGBA color for every
        vertex

    Red, Green, Blue and Alpha channels may be defined by specifying a function
    for each.

    See help(mlx.muparser_ref) for muparser reference documentation.

    It's possible to use the following per-vertex variables in the expression:

    Variables (per vertex):
        x, y, z (coordinates)
        nx, ny, nz (normal)
        r, g, b, a (color)
        q (quality)
        rad (radius)
        vi (vertex index)
        vtu, vtv (texture coordinates)
        ti (texture index)
        vsel (is the vertex selected? 1 yes, 0 no)
        and all custom vertex attributes already defined by user.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        red (str [0, 255]): function to generate red component
        green (str [0, 255]): function to generate green component
        blue (str [0, 255]): function to generate blue component
        alpha (str [0, 255]): function to generate alpha component
        color (str): name of one of the 140 HTML Color Names defined
            in CSS & SVG.
            Ref: https://en.wikipedia.org/wiki/Web_colors#X11_color_names
            If not None this will override the per component variables.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    # TODO: add options for HSV
    # https://www.cs.rit.edu/~ncs/color/t_convert.html
    if color is not None:
        red, green, blue, _ = color_name[color.lower()]
    filter_xml = ''.join([
        '  <filter name="Per Vertex Color Function">\n',
        '    <Param name="x" ',
        'value="{}" '.format(str(red).replace('&', '&').replace('<', '<')),
        'description="func r = " ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="y" ',
        'value="{}" '.format(str(green).replace('&', '&').replace('<', '<')),
        'description="func g = " ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="z" ',
        'value="{}" '.format(str(blue).replace('&', '&').replace('<', '<')),
        'description="func b = " ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="a" ',
        'value="{}" '.format(str(alpha).replace('&', '&').replace('<', '<')),
        'description="func alpha = " ',
        'type="RichString" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

def voronoi(script, target_layer=0, source_layer=1, backward=True):
    """ Given a Mesh 'M' and a Pointset 'P', the filter projects each vertex of
        P over M and color M according to the geodesic distance from these
        projected points. Projection and coloring are done on a per vertex
        basis.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        target_layer (int): The mesh layer whose surface is colored. For each
            vertex of this mesh we decide the color according to the following
            arguments.
        source_layer (int): The mesh layer whose vertexes are used as seed
            points for the color computation. These seeds point are projected
            onto the target_layer mesh.
        backward (bool): If True the mesh is colored according to the distance
            from the frontier of the voronoi diagram induced by the
            source_layer seeds.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Voronoi Vertex Coloring">\n',
        '    <Param name="ColoredMesh" ',
        'value="{:d}" '.format(target_layer),
        'description="To be Colored Mesh" ',
        'type="RichMesh" ',
        '/>\n',
        '    <Param name="VertexMesh" ',
        'value="{:d}" '.format(source_layer),
        'description="Vertex Mesh" ',
        'type="RichMesh" ',
        '/>\n',
        '    <Param name="backward" ',
        'value="{}" '.format(str(backward).lower()),
        'description="BackDistance" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

def cyclic_rainbow(script, direction='sphere', start_pt=(0, 0, 0),
                   amplitude=255 / 2, center=255 / 2, freq=0.8,
                   phase=(0, 120, 240, 0), alpha=False):
    """ Color mesh vertices in a repeating sinusiodal rainbow pattern

    Sine wave follows the following equation for each color channel (RGBA):
    channel = sin(freq*increment + phase)*amplitude + center

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        direction (str) = the direction that the sine wave will travel; this
            and the start_pt determine the 'increment' of the sine function.
            Valid values are:
            'sphere' - radiate sine wave outward from start_pt (default)
            'x' - sine wave travels along the X axis
            'y' - sine wave travels along the Y axis
            'z' - sine wave travels along the Z axis
            or define the increment directly using a muparser function, e.g.
                '2x + y'. In this case start_pt will not be used; include it in
                the function directly.
    start_pt (3 coordinate tuple or list): start point of the sine wave. For a
        sphere this is the center of the sphere.
    amplitude (float [0, 255], single value or 4 term tuple or list): amplitude
        of the sine wave, with range between 0-255. If a single value is
        specified it will be used for all channels, otherwise specify each
        channel individually.
    center (float [0, 255], single value or 4 term tuple or list): center
        of the sine wave, with range between 0-255. If a single value is
        specified it will be used for all channels, otherwise specify each
        channel individually.
    freq (float, single value or 4 term tuple or list): frequency of the sine
        wave. If a single value is specified it will be used for all channels,
        otherwise specifiy each channel individually.
    phase (float [0, 360], single value or 4 term tuple or list): phase
        of the sine wave in degrees, with range between 0-360. If a single
        value is specified it will be used for all channels, otherwise specify
        each channel individually.
    alpha (bool): if False the alpha channel will be set to 255 (full opacity).

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    start_pt = util.make_list(start_pt, 3)
    amplitude = util.make_list(amplitude, 4)
    center = util.make_list(center, 4)
    freq = util.make_list(freq, 4)
    phase = util.make_list(phase, 4)

    if direction.lower() == 'sphere':
        increment = 'sqrt((x-{})^2+(y-{})^2+(z-{})^2)'.format(
            start_pt[0], start_pt[1], start_pt[2])
    elif direction.lower() == 'x':
        increment = 'x - {}'.format(start_pt[0])
    elif direction.lower() == 'y':
        increment = 'y - {}'.format(start_pt[1])
    elif direction.lower() == 'z':
        increment = 'z - {}'.format(start_pt[2])
    else:
        increment = direction

    red_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
        f=freq[0], i=increment, p=math.radians(phase[0]),
        a=amplitude[0], c=center[0])
    green_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
        f=freq[1], i=increment, p=math.radians(phase[1]),
        a=amplitude[1], c=center[1])
    blue_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
        f=freq[2], i=increment, p=math.radians(phase[2]),
        a=amplitude[2], c=center[2])
    if alpha:
        alpha_func = '{a}*sin({f}*{i} + {p}) + {c}'.format(
            f=freq[3], i=increment, p=math.radians(phase[3]),
            a=amplitude[3], c=center[3])
    else:
        alpha_func = 255

    function(script, red=red_func, green=green_func, blue=blue_func,
             alpha=alpha_func)
    return None


""" MeshLabXML muparser functions """

import math
import re

from . import FilterScript
from . import util

def muparser_ref():
    """Reference documentation for muparser.

    muparser is used by many internal MeshLab filters, specifically those
    where you can control parameters via a mathematical expression. Examples:
        transform.function
        select.vert_function
        vert_color.function

    The valid variables that can be used in an expression are given in the
    documentation for the individual functions. Generally, it's possible to use
    the following per-vertex variables in the expression:

    Variables (per vertex):
        x, y, z (coordinates)
        nx, ny, nz (normal)
        r, g, b, a (color)
        q (quality)
        rad
        vi (vertex index)
        vtu, vtv (texture coordinates)
        ti (texture index)
        vsel (is the vertex selected? 1 yes, 0 no)
        and all custom vertex attributes already defined by user.

    Below is a list of the predefined operators and functions within muparser.

    muparser homepage: http://beltoforion.de/article.php?a=muparser
    ml_version='1.3.4Beta' muparser version: 1.3.2
    ml_version='2016.12' muparser version: 2.2.5

    Built-in functions:
        Name    Argc.   Explanation
        ----------------------------
        sin     1       sine function
        cos     1       cosine function
        tan     1       tangens function
        asin    1       arcus sine function
        acos    1       arcus cosine function
        atan    1       arcus tangens function
        atan2   2       *
        sinh    1       hyperbolic sine function
        cosh    1       hyperbolic cosine
        tanh    1       hyperbolic tangens function
        asinh   1       hyperbolic arcus sine function
        acosh   1       hyperbolic arcus tangens function
        atanh   1       hyperbolic arcur tangens function
        log2    1       logarithm to the base 2
        log10   1       logarithm to the base 10
        log     1       logarithm to the base 10
        ln      1       logarithm to base e (2.71828...)
        exp     1       e raised to the power of x
        sqrt    1       square root of a value
        sign    1       sign function -1 if x<0; 1 if x>0
        rint    1       round to nearest integer
        abs     1       absolute value
        min     var.    min of all arguments
        max     var.    max of all arguments
        sum     var.    sum of all arguments
        avg     var.    mean value of all arguments

    Built-in binary operators
        Operator    Description                Priority
        -----------------------------------------------
        =           assignment                 -1
        &&          logical and*                1
        ||          logical or*                 2
        <=          less or equal               4
        >=          greater or equal            4
        !=          not equal                   4
        ==          equal                       4
        >           greater than                4
        <           less than                   4
        +           addition                    5
        -           subtraction                 5
        *           multiplication              6
        /           division                    6
        ^           raise x to the power of y   7

    Built-in ternary operator
        ?:         (Test ? Then_value : Otherwise_value)*

    *: Some operators in older muparser (ml_version='1.3.4Beta') are different:
        and        logical and                 1
        or         logical or                  2
        if         ternary operator: if(Test, Then_value, Otherwise_value)
        atan2      not included; use mp_atan2 below
    """
    pass

def mp_atan2(y, x):
    """muparser atan2 function

    Implements an atan2(y,x) function for older muparser versions (<2.1.0);
    atan2 was added as a built-in function in muparser 2.1.0

    Args:
        y (str): y argument of the atan2(y,x) function
        x (str): x argument of the atan2(y,x) function

    Returns:
        A muparser string that calculates atan2(y,x)
    """
    return 'if((x)>0, atan((y)/(x)), if(((x)<0) and ((y)>=0), atan((y)/(x))+pi, if(((x)<0) and ((y)<0), atan((y)/(x))-pi, if(((x)==0) and ((y)>0), pi/2, if(((x)==0) and ((y)<0), -pi/2, 0)))))'.replace(
        'pi', str(math.pi)).replace('y', y).replace('x', x)

def v_cross(u, v):
    """muparser cross product function

    Compute the cross product of two 3x1 vectors

    Args:
        u (list or tuple of 3 strings): first vector
        v (list or tuple of 3 strings): second vector
    Returns:
        A list containing a muparser string of the cross product
    """
    """
    i = u[1]*v[2] - u[2]*v[1]
    j = u[2]*v[0] - u[0]*v[2]
    k = u[0]*v[1] - u[1]*v[0]
    """

    i = '(({u1})*({v2}) - ({u2})*({v1}))'.format(u1=u[1], u2=u[2], v1=v[1], v2=v[2])
    j = '(({u2})*({v0}) - ({u0})*({v2}))'.format(u0=u[0], u2=u[2], v0=v[0], v2=v[2])
    k = '(({u0})*({v1}) - ({u1})*({v0}))'.format(u0=u[0], u1=u[1], v0=v[0], v1=v[1])
    return [i, j, k]

def v_dot(v1, v2):
    for i, x in enumerate(v1):
        if i == 0:
            dot = '({})*({})'.format(v1[i], v2[i])
        else:
            dot += '+({})*({})'.format(v1[i], v2[i])
    dot = '(' + dot + ')'
    return dot

def v_add(v1, v2):
    vector = []
    for i, x in enumerate(v1):
        vector.append('(({})+({}))'.format(v1[i], v2[i]))
    return vector

def v_subtract(v1, v2):
    vector = []
    for i, x in enumerate(v1):
        vector.append('(({})-({}))'.format(v1[i], v2[i]))
    return vector

def v_multiply(scalar, v1):
    """ Multiply vector by scalar"""
    vector = []
    for i, x in enumerate(v1):
        vector.append('(({})*({}))'.format(scalar, v1[i]))
    return vector

def v_length(v1):
    for i, x in enumerate(v1):
        if i == 0:
            length = '({})^2'.format(v1[i])
        else:
            length += '+({})^2'.format(v1[i])
    length = 'sqrt(' + length + ')'
    return length

def v_normalize(v1):
    vector = []
    length = v_length(v1)
    for i, x in enumerate(v1):
        vector.append('({})/({})'.format(v1[i], length))
    return vector

def torus_knot(t, p=3, q=4, scale=1.0, radius=2.0):
    """ A tight (small inner crossings) (p,q) torus knot parametric curve

    Source (for trefoil): https://en.wikipedia.org/wiki/Trefoil_knot

    """
    return ['{scale}*(sin({t}) + ({radius})*sin({p}*({t})))'.format(t=t, p=p, scale=scale, radius=radius),
            '{scale}*(cos({t}) - ({radius})*cos({p}*({t})))'.format(t=t, p=p, scale=scale, radius=radius),
            '{scale}*(-sin({q}*({t})))'.format(t=t, q=q, scale=scale)]

def torus_knot_bbox(scale=1.0, radius=2.0):
    """ Bounding box of the sprecified torus knot

    """
    return [2*scale*(1 + radius), 2*scale*(1 + radius), 2*scale]

def vert_attr(script, name='radius', function='x^2 + y^2'):
    """ Add a new Per-Vertex scalar attribute to current mesh and fill it with
        the defined function.

    The specified name can be used in other filter functions.

    It's possible to use parenthesis, per-vertex variables and boolean operator:
        (, ), and, or, <, >, =
    It's possible to use the following per-vertex variables in the expression:

    Variables:
        x, y, z (coordinates)
        nx, ny, nz (normal)
        r, g, b, a (color)
        q (quality)
        rad
        vi (vertex index)
        ?vtu, vtv (texture coordinates)
        ?ti (texture index)
        ?vsel (is the vertex selected? 1 yes, 0 no)
        and all custom vertex attributes already defined by user.

    Args:
        script: the FilterScript object or script filename to write
            the filter] to.
        name (str): the name of new attribute. You can access attribute in
            other filters through this name.
        function (str): function to calculate custom attribute value for each
            vertex

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Define New Per Vertex Attribute">\n',
        '    <Param name="name" ',
        'value="{}" '.format(name),
        'description="Name" ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="expr" ',
        'value="{}" '.format(str(function).replace('&', '&').replace('<', '<')),
        'description="Function" ',
        'type="RichString" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

def face_attr(script, name='radiosity', function='fi'):
    """ Add a new Per-Face attribute to current mesh and fill it with
        the defined function..

    The specified name can be used in other filter functions.

    It's possible to use parenthesis, per-face variables and boolean operator:
        (, ), and, or, <, >, =

    It's possible to use per-face variables like attributes associated to the
    three vertices of every face.

    It's possible to use the following per-face variables in the expression:

    Variables:
        x0,y0,z0 (first vertex); x1,y1,z1 (second vertex); x2,y2,z2 (third vertex)
        nx0,ny0,nz0; nx1,ny1,nz1; nx2,ny2,nz2 (normals)
        r0,g0,b0 (color)
        q0,q1,q2 (quality)
        wtu0, wtv0; wtu1, wtv1; wtu2, wtv2 (per wedge tex coord)
        fi (face index)
        ?fsel (is the vertex selected? 1 yes, 0 no)
        fi (face index)
        and all custom face attributes already defined by user.

    Args:
        script: the FilterScript object or script filename to write
            the filter] to.
        name (str): the name of new attribute. You can access attribute in
            other filters through this name.
        function (str): function to calculate custom attribute value for each
            face

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Define New Per Face Attribute">\n',
        '    <Param name="name" ',
        'value="{}" '.format(name),
        'description="Name" ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="expr" ',
        'value="{}" '.format(str(function).replace('&', '&').replace('<', '<')),
        'description="Function" ',
        'type="RichString" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

def vq_function(script, function='vi', normalize=False, color=False):
    """:Quality function using muparser to generate new Quality for every vertex
It's possibile to use the following per-vertex variables in the expression:
x, y, z, nx, ny, nz (normal), r, g, b (color), q (quality), rad, vi,
and all custom vertex attributes already defined by user.

    function function to generate new Quality for every vertex
    normalize if checked normalize all quality values in range [0..1]
    color if checked map quality generated values into per-vertex color

    """
    filter_xml = ''.join([
        '  <filter name="Per Vertex Quality Function">\n',
        '    <Param name="q" ',
        'value="{}" '.format(str(function).replace('&', '&').replace('<', '<')),
        'description="func q = " ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="normalize" ',
        'value="{}" '.format(str(normalize).lower()),
        'description="normalize" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="map" ',
        'value="{}" '.format(str(color).lower()),
        'description="map into color" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

def fq_function(script, function='x0+y0+z0', normalize=False, color=False):
    """ :Quality function using muparser to generate new Quality for every face
Insert three function each one for quality of the three vertex of a face
It's possibile to use per-face variables like attributes associated to the three vertex of every face.
x0,y0,z0 for first vertex; x1,y1,z1 for second vertex; x2,y2,z2 for third vertex.
and also nx0,ny0,nz0 nx1,ny1,nz1 etc. for normals and r0,g0,b0 for color,q0,q1,q2 for quality.

    function function to generate new Quality for each face
    normalize if checked normalize all quality values in range [0..1]
    color if checked map quality generated values into per-vertex color

    """
    filter_xml = ''.join([
        '  <filter name="Per Face Quality Function">\n',
        '    <Param name="q" ',
        'value="{}" '.format(str(function).replace('&', '&').replace('<', '<')),
        'description="func q0 = " ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="normalize" ',
        'value="{}" '.format(str(normalize).lower()),
        'description="normalize" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="map" ',
        'value="{}" '.format(str(color).lower()),
        'description="map into color" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None


#!/usr/bin/python3
""" MLX logo and example script

Demonstrates how to use MLX as a design tool for a moderately complex project.

Units: mm

License:
    Copyright (C) 2017 by Tim Ayres, 3DLirious@gmail.com

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
"""

import os
import time
import inspect
import math

import meshlabxml as mlx

THIS_SCRIPTPATH = os.path.dirname(
    os.path.realpath(inspect.getsourcefile(lambda: 0)))

def quatrefoil():
    """ Rainbow colored voronoi quatrefoil (3,4) torus knot """
    start_time = time.time()

    os.chdir(THIS_SCRIPTPATH)
    #ml_version = '1.3.4BETA'
    ml_version = '2016.12'

    # Add meshlabserver directory to OS PATH; omit this if it is already in
    # your PATH
    meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab'
    """
    if ml_version is '1.3.4BETA':
        meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab'
    elif ml_version is '2016.12':
        meshlabserver_path = 'C:\\Program Files\\VCG\\MeshLab_2016_12'
    """
    os.environ['PATH'] = meshlabserver_path + os.pathsep + os.environ['PATH']

    # Cross section parameters
    length = math.radians(360)
    tube_size = [10, 10, length]
    segments = [64, 64, 720*2]
    inner_radius = 2.0

    # Sinusoidal deformation parametera
    amplitude = 4.2
    freq = 4
    phase = 270
    center = 'r'
    start_pt = 0
    increment = 'z-{}'.format(start_pt)

    # Cyclic rainbow color parameters
    c_start_pt = 0
    c_freq = 5
    c_phase_shift = 0 #90 #300
    c_phase = (0 + c_phase_shift, 120 + c_phase_shift, 240 + c_phase_shift, 0)

    # Voronoi surface parameters
    holes = [2, 2, 44] # Number of holes in each axis; x are sides, y is outside
    web_thickness = 0.5
    solid_radius = 5.0 # If the mesh is smaller than this radius the holes will be closed
    faces_surface = 50000

    # Voronoi solid parameters
    voxel = 0.5
    thickness = 2.5
    faces_solid = 200000

    # Scaling parameters
    size = 75 # desired max size of the curve
    curve_max_size = 2*(1 + 1.5) # the 1.5 s/b inner_radius, but am keepng current scaling
    scale = (size-2*(thickness + amplitude) - tube_size[1])/curve_max_size

    # File names
    file_color = 'quatrefoil_color.ply'
    file_voronoi_surf = 'quatrefoil_voronoi_surf.ply'
    file_voronoi_solid = 'quatrefoil_voronoi_solid.ply'
    file_voronoi_color = 'quatrefoil_voronoi_final.ply'

    # Create FilterScript objects for each step in the process
    quatrefoil_color = mlx.FilterScript(
        file_in=None, file_out=file_color, ml_version=ml_version)
    quatrefoil_voronoi_surf = mlx.FilterScript(
        file_in=file_color, file_out=file_voronoi_surf, ml_version=ml_version)
    quatrefoil_voronoi_solid = mlx.FilterScript(
        file_in=file_voronoi_surf, file_out=file_voronoi_solid,
        ml_version=ml_version)
    quatrefoil_voronoi_color = mlx.FilterScript(
        file_in=[file_color, file_voronoi_solid], file_out=file_voronoi_color,
        ml_version=ml_version)

    print('\n Create colored quatrefoil curve ...')
    mlx.create.cube_open_hires(
        quatrefoil_color, size=tube_size, x_segments=segments[0],
        y_segments=segments[1], z_segments=segments[2], center=True)
    mlx.transform.translate(quatrefoil_color, [0, 0, length/2])

    # Sinusoidal deformation
    r_func = '({a})*sin(({f})*({i}) + ({p})) + ({c})'.format(
        f=freq, i=increment, p=math.radians(phase), a=amplitude, c=center)
    mlx.transform.function_cyl_co(
        quatrefoil_color, r_func=r_func, theta_func='theta', z_func='z')

    # Save max radius in quality field so that we can save it with the file
    #  for use in the next step
    max_radius = math.sqrt((tube_size[0]/2)**2+(tube_size[1]/2)**2) # at corners
    q_func = '({a})*sin(({f})*({i}) + ({p})) + ({c})'.format(
        f=freq, i=increment, p=math.radians(phase), a=amplitude, c=max_radius)
    mlx.mp_func.vq_function(quatrefoil_color, function=q_func)

    # Apply rainbow vertex colors
    mlx.vert_color.cyclic_rainbow(
        quatrefoil_color, direction='z', start_pt=c_start_pt, amplitude=255 / 2,
        center=255 / 2, freq=c_freq, phase=c_phase)

    # Deform mesh to quatrefoil curve. Merge vertices after, which
    # will weld the ends together so it becomes watertight
    quatrefoil_func = mlx.transform.deform2curve(
        quatrefoil_color,
        curve=mlx.mp_func.torus_knot('t', p=3, q=4, scale=scale,
                                     radius=inner_radius))
    mlx.clean.merge_vert(quatrefoil_color, threshold=0.0001)

    # Run script
    mlx.layers.delete_lower(quatrefoil_color)
    quatrefoil_color.run_script(output_mask='-m vc vq')

    print('\n Create Voronoi surface ...')
    # Move quality value into radius attribute
    mlx.mp_func.vert_attr(quatrefoil_voronoi_surf, name='radius', function='q')

    # Create seed vertices
    # For grid style holes, we will create a mesh similar to the original
    # but with fewer vertices.
    mlx.create.cube_open_hires(
        quatrefoil_voronoi_surf, size=tube_size, x_segments=holes[0]+1,
        y_segments=holes[1]+1, z_segments=holes[2]+1, center=True)
    mlx.select.all(quatrefoil_voronoi_surf, vert=False)
    mlx.delete.selected(quatrefoil_voronoi_surf, vert=False)
    mlx.select.cylindrical_vert(quatrefoil_voronoi_surf,
                                radius=max_radius-0.0001, inside=False)
    mlx.transform.translate(quatrefoil_voronoi_surf, [0, 0, 20])
    mlx.delete.selected(quatrefoil_voronoi_surf, face=False)

    mlx.transform.function_cyl_co(quatrefoil_voronoi_surf, r_func=r_func,
                                  theta_func='theta', z_func='z')
    mlx.transform.vert_function(
        quatrefoil_voronoi_surf, x_func=quatrefoil_func[0],
        y_func=quatrefoil_func[1], z_func=quatrefoil_func[2])

    mlx.layers.change(quatrefoil_voronoi_surf, 0)
    mlx.vert_color.voronoi(quatrefoil_voronoi_surf)

    if quatrefoil_voronoi_surf.ml_version == '1.3.4BETA':
        sel_func = '(q <= {}) or ((radius)<={})'.format(web_thickness, solid_radius)
    else:
        sel_func = '(q <= {}) || ((radius)<={})'.format(web_thickness, solid_radius)
    mlx.select.vert_function(quatrefoil_voronoi_surf, function=sel_func)
    #mlx.select.face_function(quatrefoil_voronoi_surf, function='(vsel0 && vsel1 && vsel2)')
    mlx.select.invert(quatrefoil_voronoi_surf, face=False)
    mlx.delete.selected(quatrefoil_voronoi_surf, face=False)

    mlx.smooth.laplacian(quatrefoil_voronoi_surf, iterations=3)
    mlx.remesh.simplify(quatrefoil_voronoi_surf, texture=False, faces=faces_surface)

    mlx.layers.delete_lower(quatrefoil_voronoi_surf)
    #quatrefoil_voronoi_surf.save_to_file('temp_script.mlx')
    quatrefoil_voronoi_surf.run_script(script_file=None, output_mask='-m vc vq')

    print('\n Solidify Voronoi surface ...')
    mlx.remesh.uniform_resampling(quatrefoil_voronoi_solid, voxel=voxel,
                                  offset=thickness/2, thicken=True)
    mlx.layers.delete_lower(quatrefoil_voronoi_solid)
    quatrefoil_voronoi_solid.run_script()

    print('\n Clean up & transfer color to final model ...')
    # Clean up from uniform mesh resamplng
    mlx.delete.small_parts(quatrefoil_voronoi_color)
    mlx.delete.unreferenced_vert(quatrefoil_voronoi_color)
    mlx.delete.faces_from_nonmanifold_edges(quatrefoil_voronoi_color)
    mlx.clean.split_vert_on_nonmanifold_face(quatrefoil_voronoi_color)
    mlx.clean.close_holes(quatrefoil_voronoi_color)

    # Simplify (to improve triangulation quality), refine, & smooth
    mlx.remesh.simplify(quatrefoil_voronoi_color, texture=False, faces=faces_solid)
    mlx.subdivide.ls3loop(quatrefoil_voronoi_color, iterations=1)
    mlx.smooth.laplacian(quatrefoil_voronoi_color, iterations=3)

    # Transfer colors from original curve
    mlx.transfer.vert_attr_2_meshes(
        quatrefoil_voronoi_color, source_mesh=0, target_mesh=1, color=True,
        max_distance=7)
    mlx.layers.delete_lower(quatrefoil_voronoi_color)
    quatrefoil_voronoi_color.run_script(script_file=None)
    print('    done! Took %.1f sec' % (time.time() - start_time))

    return None

if __name__ == '__main__':
    quatrefoil()


""" MeshLabXML functions for mesh normals """

from . import util

def reorient(script):
    """ Re-orient in a consistent way all the faces of the mesh.

    The filter visits a mesh face to face, reorienting any unvisited face so
    that it is coherent to the already visited faces. If the surface is
    orientable it will end with a consistent orientation of all the faces. If
    the surface is not orientable (e.g. it is non manifold or non orientable
    like a moebius strip) the filter will not build a consistent orientation
    simply because it is not possible. The filter can end up in a consistent
    orientation that can be exactly the opposite of the expected one; in that
    case simply invert the whole mesh orientation.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = '  <filter name="Re-Orient all faces coherentely"/>\n'
    util.write_filter(script, filter_xml)
    return None

def flip(script, force_flip=False, selected=False):
    """ Invert faces orientation, flipping the normals of the mesh.

    If requested, it tries to guess the right orientation; mainly it decides to
    flip all the faces if the minimum/maximum vertexes have not outward point
    normals for a few directions. Works well for single component watertight
    objects.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        force_flip (bool): If selected, the normals will always be flipped;
            otherwise, the filter tries to set them outside.
        selected (bool): If selected, only selected faces will be affected.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Invert Faces Orientation">\n',
        '    <Param name="forceFlip" ',
        'value="{}" '.format(str(force_flip).lower()),
        'description="Force Flip" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="onlySelected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Flip only selected faces" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

def fix(script):
    """ Will reorient normals & ensure they are oriented outwards

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    reorient(script)
    flip(script)
    return

def point_sets(script, neighbors=10, smooth_iteration=0, flip=False,
               viewpoint_pos=(0.0, 0.0, 0.0)):
    """ Compute the normals of the vertices of a mesh without exploiting the
        triangle connectivity, useful for dataset with no faces.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        neighbors (int): The number of neighbors used to estimate normals.
        smooth_iteration (int): The number of smoothing iteration done on the
            p used to estimate and propagate normals.
        flip (bool): Flip normals w.r.t. viewpoint. If the 'viewpoint' (i.e.
            scanner position) is known, it can be used to disambiguate normals
            orientation, so that all the normals will be oriented in the same
            direction.
        viewpoint_pos (single xyz point, tuple or list): Set the x, y, z
            coordinates of the viewpoint position.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Compute normals for point sets">\n',
        '    <Param name="K" ',
        'value="{:d}" '.format(neighbors),
        'description="Neighbour num" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="smoothIter" ',
        'value="{:d}" '.format(smooth_iteration),
        'description="Smooth Iteration" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="flipFlag" ',
        'value="{}" '.format(str(flip).lower()),
        'description="Flip normals w.r.t. viewpoint" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="viewPos" ',
        'x="{}" y="{}" z="{}" '.format(viewpoint_pos[0], viewpoint_pos[1],
                                       viewpoint_pos[2],),
        'description="Viewpoint Pos." ',
        'type="RichPoint3f" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None


""" MeshLabXML smoothing functions """

from . import util

def laplacian(script, iterations=1, boundary=True, cotangent_weight=True,
              selected=False):
    """ Laplacian smooth of the mesh: for each vertex it calculates the average
        position with nearest vertex

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        iterations (int): The number of times that the whole algorithm (normal
            smoothing + vertex fitting) is iterated.
        boundary (bool): If true the boundary edges are smoothed only by
            themselves (e.g. the polyline forming the boundary of the mesh is
            independently smoothed). Can reduce the shrinking on the border but
            can have strange effects on very small boundaries.
        cotangent_weight (bool): If True the cotangent weighting scheme is
            computed for the averaging of the position. Otherwise (False) the
            simpler umbrella scheme (1 if the edge is present) is used.
        selected (bool): If selected the filter is performed only on the
            selected faces

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Laplacian Smooth">\n',
        '    <Param name="stepSmoothNum" ',
        'value="{:d}" '.format(iterations),
        'description="Smoothing steps" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="Boundary" ',
        'value="{}" '.format(str(boundary).lower()),
        'description="1D Boundary Smoothing" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="cotangentWeight" ',
        'value="{}" '.format(str(cotangent_weight).lower()),
        'description="Cotangent weighting" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="Selected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Affect only selected faces" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

def hc_laplacian(script):
    """ HC Laplacian Smoothing, extended version of Laplacian Smoothing, based
        on the paper of Vollmer, Mencl, and Muller

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = '  <filter name="HC Laplacian Smooth"/>\n'
    util.write_filter(script, filter_xml)
    return None

def taubin(script, iterations=10, t_lambda=0.5, t_mu=-0.53, selected=False):
    """ The lambda & mu Taubin smoothing, it make two steps of smoothing, forth
        and back, for each iteration.

    Based on:
    Gabriel Taubin
    "A signal processing approach to fair surface design"
    Siggraph 1995

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        iterations (int): The number of times that the taubin smoothing is
            iterated. Usually it requires a larger number of iteration than the
            classical laplacian.
        t_lambda (float): The lambda parameter of the Taubin Smoothing algorithm
        t_mu (float): The mu parameter of the Taubin Smoothing algorithm
        selected (bool): If selected the filter is performed only on the
            selected faces

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Taubin Smooth">\n',
        '    <Param name="lambda" ',
        'value="{}" '.format(t_lambda),
        'description="Lambda" ',
        'type="RichFloat" ',
        '/>\n',
        '    <Param name="mu" ',
        'value="{}" '.format(t_mu),
        'description="mu" ',
        'type="RichFloat" ',
        '/>\n',
        '    <Param name="stepSmoothNum" ',
        'value="{:d}" '.format(iterations),
        'description="Smoothing steps" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="Selected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Affect only selected faces" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

def twostep(script, iterations=3, angle_threshold=60, normal_steps=20, fit_steps=20,
            selected=False):
    """ Two Step Smoothing, a feature preserving/enhancing fairing filter.

    It is based on a Normal Smoothing step where similar normals are averaged
    together and a step where the vertexes are fitted on the new normals.

    Based on:
    A. Belyaev and Y. Ohtake,
    "A Comparison of Mesh Smoothing Methods"
    Proc. Israel-Korea Bi-National Conf. Geometric Modeling and Computer
    Graphics, pp. 83-87, 2003.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        iterations (int): The number of times that the whole algorithm (normal
            smoothing + vertex fitting) is iterated.
        angle_threshold (float): Specify a threshold angle (0..90) for features
            that you want to be preserved.  Features forming angles LARGER than
            the specified threshold will be preserved.
            0 -> no smoothing
            90 -> all faces will be smoothed
        normal_steps (int): Number of iterations of normal smoothing step. The
            larger the better and (the slower)
        fit_steps (int): Number of iterations of the vertex fitting procedure
        selected (bool): If selected the filter is performed only on the
            selected faces

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="TwoStep Smooth">\n',
        '    <Param name="stepSmoothNum" ',
        'value="{:d}" '.format(iterations),
        'description="Smoothing steps" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="normalThr" ',
        'value="{}" '.format(angle_threshold),
        'description="Feature Angle Threshold (deg)" ',
        'type="RichFloat" ',
        '/>\n',
        '    <Param name="stepNormalNum" ',
        'value="{:d}" '.format(normal_steps),
        'description="Normal Smoothing steps" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="stepFitNum" ',
        'value="{:d}" '.format(fit_steps),
        'description="Vertex Fitting steps" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="Selected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Affect only selected faces" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

def depth(script, iterations=3, viewpoint=(0, 0, 0), selected=False):
    """ A laplacian smooth that is constrained to move vertices only along the
        view direction.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        iterations (int): The number of times that the whole algorithm (normal
            smoothing + vertex fitting) is iterated.
        viewpoint (vector tuple or list): The position of the view point that
            is used to get the constraint direction.
        selected (bool): If selected the filter is performed only on the
            selected faces

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Depth Smooth">\n',
        '    <Param name="stepSmoothNum" ',
        'value="{:d}" '.format(iterations),
        'description="Smoothing steps" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="viewPoint" ',
        'x="{}" '.format(viewpoint[0]),
        'y="{}" '.format(viewpoint[1]),
        'z="{}" '.format(viewpoint[2]),
        'description="Smoothing steps" ',
        'type="RichPoint3f" ',
        '/>\n',
        '    <Param name="Selected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Affect only selected faces" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None


""" MeshLabXML functions that operate on mesh files """

import os
import sys
import math

import meshlabxml as mlx
from . import run
from . import util
from . import compute
from . import transform
from . import layers

#ml_version = '1.3.4BETA'
ml_version = '2016.12'

def measure_aabb(fbasename=None, log=None, coord_system='CARTESIAN'):
    """ Measure the axis aligned bounding box (aabb) of a mesh
    in multiple coordinate systems.

    Args:
        fbasename (str): filename of input model
        log (str): filename of log file
        coord_system (enum in ['CARTESIAN', 'CYLINDRICAL']
            Coordinate system to use:
                'CARTESIAN': lists contain [x, y, z]
                'CYLINDRICAL': lists contain [r, theta, z]
    Returns:
        dict: dictionary with the following aabb properties
            min (3 element list): minimum values
            max (3 element list): maximum values
            center (3 element list): the center point
            size (3 element list): size of the aabb in each coordinate (max-min)
            diagonal (float): the diagonal of the aabb
    """
    # TODO: add center point, spherical coordinate system
    fext = os.path.splitext(fbasename)[1][1:].strip().lower()
    if fext != 'xyz':
        fin = 'TEMP3D_aabb.xyz'
        run(log=log, file_in=fbasename, file_out=fin, script=None)
    else:
        fin = fbasename
    fread = open(fin, 'r')
    aabb = {'min': [999999.0, 999999.0, 999999.0], 'max': [-999999.0, -999999.0, -999999.0]}
    for line in fread:
        x_co, y_co, z_co = line.split()
        x_co = util.to_float(x_co)
        y_co = util.to_float(y_co)
        z_co = util.to_float(z_co)
        if coord_system == 'CARTESIAN':
            if x_co < aabb['min'][0]:
                aabb['min'][0] = x_co
            if y_co < aabb['min'][1]:
                aabb['min'][1] = y_co
            if z_co < aabb['min'][2]:
                aabb['min'][2] = z_co
            if x_co > aabb['max'][0]:
                aabb['max'][0] = x_co
            if y_co > aabb['max'][1]:
                aabb['max'][1] = y_co
            if z_co > aabb['max'][2]:
                aabb['max'][2] = z_co
        elif coord_system == 'CYLINDRICAL':
            radius = math.sqrt(x_co**2 + y_co**2)
            theta = math.degrees(math.atan2(y_co, x_co))
            if radius < aabb['min'][0]:
                aabb['min'][0] = radius
            if theta < aabb['min'][1]:
                aabb['min'][1] = theta
            if z_co < aabb['min'][2]:
                aabb['min'][2] = z_co
            if radius > aabb['max'][0]:
                aabb['max'][0] = radius
            if theta > aabb['max'][1]:
                aabb['max'][1] = theta
            if z_co > aabb['max'][2]:
                aabb['max'][2] = z_co
    fread.close()
    try:
        aabb['center'] = [(aabb['max'][0] + aabb['min'][0]) / 2,
                          (aabb['max'][1] + aabb['min'][1]) / 2,
                          (aabb['max'][2] + aabb['min'][2]) / 2]
        aabb['size'] = [aabb['max'][0] - aabb['min'][0], aabb['max'][1] - aabb['min'][1],
                        aabb['max'][2] - aabb['min'][2]]
        aabb['diagonal'] = math.sqrt(
            aabb['size'][0]**2 +
            aabb['size'][1]**2 +
            aabb['size'][2]**2)
    except UnboundLocalError:
        print('Error: aabb input file does not contain valid data. Exiting ...')
        sys.exit(1)
    for key, value in aabb.items():
        if log is None:
            print('{:10} = {}'.format(key, value))
        else:
            log_file = open(log, 'a')
            log_file.write('{:10} = {}\n'.format(key, value))
            log_file.close()
    """
    if log is not None:
        log_file = open(log, 'a')
        #log_file.write('***Axis Aligned Bounding Results for file "%s":\n' % fbasename)
        log_file.write('min = %s\n' % aabb['min'])
        log_file.write('max = %s\n' % aabb['max'])
        log_file.write('center = %s\n' % aabb['center'])
        log_file.write('size = %s\n' % aabb['size'])
        log_file.write('diagonal = %s\n\n' % aabb['diagonal'])
        log_file.close()
    # print(aabb)
    """
    return aabb

def measure_section(fbasename=None, log=None, axis='z', offset=0.0,
                    rotate_x_angle=None, ml_version=ml_version):
    """Measure a cross section of a mesh

    Perform a plane cut in one of the major axes (X, Y, Z). If you want to cut on
    a different plane you will need to rotate the model in place, perform the cut,
    and rotate it back.

    Args:
        fbasename (str): filename of input model
        log (str): filename of log file
        axis (str): axis perpendicular to the cutting plane, e.g. specify "z" to cut
            parallel to the XY plane.
        offset (float): amount to offset the cutting plane from the origin
        rotate_x_angle (float): degrees to rotate about the X axis. Useful for correcting "Up" direction: 90 to rotate Y to Z, and -90 to rotate Z to Y.

    Returns:
        dict: dictionary with the following keys for the aabb of the section:
            min (list): list of the x, y & z minimum values
            max (list): list of the x, y & z maximum values
            center (list): the x, y & z coordinates of the center of the aabb
            size (list): list of the x, y & z sizes (max - min)
            diagonal (float): the diagonal of the aabb
    """
    ml_script1_file = 'TEMP3D_measure_section.mlx'
    file_out = 'TEMP3D_sect_aabb.xyz'

    ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version)
    if rotate_x_angle is not None:
        transform.rotate(ml_script1, axis='x', angle=rotate_x_angle)
    compute.section(ml_script1, axis=axis, offset=offset)
    layers.delete_lower(ml_script1)
    ml_script1.save_to_file(ml_script1_file)
    ml_script1.run_script(log=log, script_file=ml_script1_file)
    aabb = measure_aabb(file_out, log)
    return aabb

def polylinesort(fbasename=None, log=None):
    """Sort separate line segments in obj format into a continuous polyline or polylines.
    NOT FINISHED; DO NOT USE

    Also measures the length of each polyline

    Return polyline and polylineMeta (lengths)

    """
    fext = os.path.splitext(fbasename)[1][1:].strip().lower()
    if fext != 'obj':
        print('Input file must be obj. Exiting ...')
        sys.exit(1)
    fread = open(fbasename, 'r')
    first = True
    polyline_vertices = []
    line_segments = []
    for line in fread:
        element, x_co, y_co, z_co = line.split()
        if element == 'v':
            polyline_vertices.append(
                [util.to_float(x_co), util.to_float(y_co), util.to_float(z_co)])
        elif element == 'l':
            p1 = x_co
            p2 = y_co
            line_segments.append([int(p1), int(p2)])

    fread.close()
    if log is not None:
        log_file = open(log, 'a')
        #log_file.write('***Axis Aligned Bounding Results for file "%s":\n' % fbasename)
        """log_file.write('min = %s\n' % aabb['min'])
        log_file.write('max = %s\n' % aabb['max'])
        log_file.write('center = %s\n' % aabb['center'])
        log_file.write('size = %s\n' % aabb['size'])
        log_file.write('diagonal = %s\n' % aabb['diagonal'])"""
        log_file.close()
    # print(aabb)
    return None

def measure_geometry(fbasename=None, log=None, ml_version=ml_version):
    """Measures mesh geometry, including aabb"""
    ml_script1_file = 'TEMP3D_measure_geometry.mlx'
    if ml_version == '1.3.4BETA':
        file_out = 'TEMP3D_aabb.xyz'
    else:
        file_out = None

    ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version)
    compute.measure_geometry(ml_script1)
    ml_script1.save_to_file(ml_script1_file)
    ml_script1.run_script(log=log, script_file=ml_script1_file)
    geometry = ml_script1.geometry

    if ml_version == '1.3.4BETA':
        if log is not None:
            log_file = open(log, 'a')
            log_file.write(
                '***Axis Aligned Bounding Results for file "%s":\n' %
                fbasename)
            log_file.close()
        aabb = measure_aabb(file_out, log)
    else:
        aabb = geometry['aabb']
    return aabb, geometry

def measure_topology(fbasename=None, log=None, ml_version=ml_version):
    """Measures mesh topology

    Args:
        fbasename (str): input filename.
        log (str): filename to log output

    Returns:
        dict: dictionary with the following keys:
            vert_num (int): number of vertices
            edge_num (int): number of edges
            face_num (int): number of faces
            unref_vert_num (int): number or unreferenced vertices
            boundry_edge_num (int): number of boundary edges
            part_num (int): number of parts (components) in the mesh.
            manifold (bool): True if mesh is two-manifold, otherwise false.
            non_manifold_edge (int): number of non_manifold edges.
            non_manifold_vert (int): number of non-manifold verices
            genus (int or str): genus of the mesh, either a number or
                'undefined' if the mesh is non-manifold.
            holes (int or str): number of holes in the mesh, either a number
                or 'undefined' if the mesh is non-manifold.

    """
    ml_script1_file = 'TEMP3D_measure_topology.mlx'
    ml_script1 = mlx.FilterScript(file_in=fbasename, ml_version=ml_version)
    compute.measure_topology(ml_script1)
    ml_script1.save_to_file(ml_script1_file)
    ml_script1.run_script(log=log, script_file=ml_script1_file)
    topology = ml_script1.topology
    return topology

def measure_all(fbasename=None, log=None, ml_version=ml_version):
    """Measures mesh geometry, aabb and topology."""
    ml_script1_file = 'TEMP3D_measure_gAndT.mlx'
    if ml_version == '1.3.4BETA':
        file_out = 'TEMP3D_aabb.xyz'
    else:
        file_out = None

    ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version)
    compute.measure_geometry(ml_script1)
    compute.measure_topology(ml_script1)
    ml_script1.save_to_file(ml_script1_file)
    ml_script1.run_script(log=log, script_file=ml_script1_file)
    geometry = ml_script1.geometry
    topology = ml_script1.topology

    if ml_version == '1.3.4BETA':
        if log is not None:
            log_file = open(log, 'a')
            log_file.write(
                '***Axis Aligned Bounding Results for file "%s":\n' %
                fbasename)
            log_file.close()
        aabb = measure_aabb(file_out, log)
    else:
        aabb = geometry['aabb']
    return aabb, geometry, topology

def measure_dimension(fbasename=None, log=None, axis1=None, offset1=0.0,
                      axis2=None, offset2=0.0, ml_version=ml_version):
    """Measure a dimension of a mesh"""
    axis1 = axis1.lower()
    axis2 = axis2.lower()
    ml_script1_file = 'TEMP3D_measure_dimension.mlx'
    file_out = 'TEMP3D_measure_dimension.xyz'

    ml_script1 = mlx.FilterScript(file_in=fbasename, file_out=file_out, ml_version=ml_version)
    compute.section(ml_script1, axis1, offset1, surface=True)
    compute.section(ml_script1, axis2, offset2, surface=False)
    layers.delete_lower(ml_script1)
    ml_script1.save_to_file(ml_script1_file)
    ml_script1.run_script(log=log, script_file=ml_script1_file)

    for val in ('x', 'y', 'z'):
        if val not in (axis1, axis2):
            axis = val
    # ord: Get number that represents letter in ASCII
    # Here we find the offset from 'x' to determine the list reference
    # i.e. 0 for x, 1 for y, 2 for z
    axis_num = ord(axis) - ord('x')
    aabb = measure_aabb(file_out, log)
    dimension = {'min': aabb['min'][axis_num], 'max': aabb['max'][axis_num],
                 'length': aabb['size'][axis_num], 'axis': axis}
    if log is None:
        print('\nFor file "%s"' % fbasename)
        print('Dimension parallel to %s with %s=%s & %s=%s:' % (axis, axis1, offset1,
                                                                axis2, offset2))
        print('  Min = %s, Max = %s, Total length = %s' % (dimension['min'],
                                                           dimension['max'], dimension['length']))
    else:
        log_file = open(log, 'a')
        log_file.write('\nFor file "%s"\n' % fbasename)
        log_file.write('Dimension parallel to %s with %s=%s & %s=%s:\n' % (axis, axis1, offset1,
                                                                           axis2, offset2))
        log_file.write('min = %s\n' % dimension['min'])
        log_file.write('max = %s\n' % dimension['max'])
        log_file.write('Total length = %s\n' % dimension['length'])
        log_file.close()
    return dimension


# -*- coding: utf-8 -*-
"""
flaskext.uploads
================
This module provides upload support for Flask. The basic pattern is to set up
an `UploadSet` object and upload your files to it.

:copyright: 2010 Matthew "LeafStorm" Frazier
:license:   MIT/X11, see LICENSE for details
"""

import sys

PY3 = sys.version_info[0] == 3

if PY3:
    string_types = str,
else:
    string_types = basestring,

import os.path
import posixpath

from flask import current_app, send_from_directory, abort, url_for
from itertools import chain
from werkzeug import secure_filename, FileStorage

from flask import Blueprint

# Extension presets

#: This just contains plain text files (.txt).
TEXT = ('txt',)

#: This contains various office document formats (.rtf, .odf, .ods, .gnumeric,
#: .abw, .doc, .docx, .xls, and .xlsx). Note that the macro-enabled versions
#: of Microsoft Office 2007 files are not included.
DOCUMENTS = tuple('rtf odf ods gnumeric abw doc docx xls xlsx pdf'.split())

#: This contains basic image types that are viewable from most browsers (.jpg,
#: .jpe, .jpeg, .png, .gif, .svg, and .bmp).
IMAGES = tuple('jpg jpe jpeg png gif svg bmp'.split())

#: This contains audio file types (.wav, .mp3, .aac, .ogg, .oga, and .flac).
AUDIO = tuple('wav mp3 aac ogg oga flac'.split())

#: This is for structured data files (.csv, .ini, .json, .plist, .xml, .yaml,
#: and .yml).
DATA = tuple('csv ini json plist xml yaml yml'.split())

#: This contains various types of scripts (.js, .php, .pl, .py .rb, and .sh).
#: If your Web server has PHP installed and set to auto-run, you might want to
#: add ``php`` to the DENY setting.
SCRIPTS = tuple('js php pl py rb sh'.split())

#: This contains archive and compression formats (.gz, .bz2, .zip, .tar,
#: .tgz, .txz, and .7z).
ARCHIVES = tuple('gz bz2 zip tar tgz txz 7z'.split())

#: This contains shared libraries and executable files (.so, .exe and .dll).
#: Most of the time, you will not want to allow this - it's better suited for
#: use with `AllExcept`.
EXECUTABLES = tuple('so exe dll'.split())

#: The default allowed extensions - `TEXT`, `DOCUMENTS`, `DATA`, and `IMAGES`.
DEFAULTS = TEXT + DOCUMENTS + IMAGES + DATA

class UploadNotAllowed(Exception):
    """
    This exception is raised if the upload was not allowed. You should catch
    it in your view code and display an appropriate message to the user.
    """

def tuple_from(*iters):
    return tuple(itertools.chain(*iters))

def extension(filename):
    ext = os.path.splitext(filename)[1]
    if ext.startswith('.'):
        # os.path.splitext retains . separator
        ext = ext[1:]
    return ext

def lowercase_ext(filename):
    """
    This is a helper used by UploadSet.save to provide lowercase extensions for
    all processed files, to compare with configured extensions in the same
    case.

    .. versionchanged:: 0.1.4
       Filenames without extensions are no longer lowercased, only the
       extension is returned in lowercase, if an extension exists.

    :param filename: The filename to ensure has a lowercase extension.
    """
    if '.' in filename:
        main, ext = os.path.splitext(filename)
        return main + ext.lower()
    # For consistency with os.path.splitext,
    # do not treat a filename without an extension as an extension.
    # That is, do not return filename.lower().
    return filename

def addslash(url):
    if url.endswith('/'):
        return url
    return url + '/'

def patch_request_class(app, size=64 * 1024 * 1024):
    """
    By default, Flask will accept uploads to an arbitrary size. While Werkzeug
    switches uploads from memory to a temporary file when they hit 500 KiB,
    it's still possible for someone to overload your disk space with a
    gigantic file.

    This patches the app's request class's
    `~werkzeug.BaseRequest.max_content_length` attribute so that any upload
    larger than the given size is rejected with an HTTP error.

    .. note::

       In Flask 0.6, you can do this by setting the `MAX_CONTENT_LENGTH`
       setting, without patching the request class. To emulate this behavior,
       you can pass `None` as the size (you must pass it explicitly). That is
       the best way to call this function, as it won't break the Flask 0.6
       functionality if it exists.

    .. versionchanged:: 0.1.1

    :param app: The app to patch the request class of.
    :param size: The maximum size to accept, in bytes. The default is 64 MiB.
                 If it is `None`, the app's `MAX_CONTENT_LENGTH` configuration
                 setting will be used to patch.
    """
    if size is None:
        if isinstance(app.request_class.__dict__['max_content_length'],
                      property):
            return
        size = app.config.get('MAX_CONTENT_LENGTH')
    reqclass = app.request_class
    patched = type(reqclass.__name__, (reqclass,),
                   {'max_content_length': size})
    app.request_class = patched

def config_for_set(uset, app, defaults=None):
    """
    This is a helper function for `configure_uploads` that extracts the
    configuration for a single set.

    :param uset: The upload set.
    :param app: The app to load the configuration from.
    :param defaults: A dict with keys `url` and `dest` from the
                     `UPLOADS_DEFAULT_DEST` and `DEFAULT_UPLOADS_URL`
                     settings.
    """
    config = app.config
    prefix = 'UPLOADED_%s_' % uset.name.upper()
    using_defaults = False
    if defaults is None:
        defaults = dict(dest=None, url=None)

    allow_extns = tuple(config.get(prefix + 'ALLOW', ()))
    deny_extns = tuple(config.get(prefix + 'DENY', ()))
    destination = config.get(prefix + 'DEST')
    base_url = config.get(prefix + 'URL')

    if destination is None:
        # the upload set's destination wasn't given
        if uset.default_dest:
            # use the "default_dest" callable
            destination = uset.default_dest(app)
        if destination is None: # still
            # use the default dest from the config
            if defaults['dest'] is not None:
                using_defaults = True
                destination = os.path.join(defaults['dest'], uset.name)
            else:
                raise RuntimeError("no destination for set %s" % uset.name)

    if base_url is None and using_defaults and defaults['url']:
        base_url = addslash(defaults['url']) + uset.name + '/'

    return UploadConfiguration(destination, base_url, allow_extns, deny_extns)

def configure_uploads(app, upload_sets):
    """
    Call this after the app has been configured. It will go through all the
    upload sets, get their configuration, and store the configuration on the
    app. It will also register the uploads module if it hasn't been set. This
    can be called multiple times with different upload sets.

    .. versionchanged:: 0.1.3
       The uploads module/blueprint will only be registered if it is needed
       to serve the upload sets.

    :param app: The `~flask.Flask` instance to get the configuration from.
    :param upload_sets: The `UploadSet` instances to configure.
    """
    if isinstance(upload_sets, UploadSet):
        upload_sets = (upload_sets,)

    if not hasattr(app, 'upload_set_config'):
        app.upload_set_config = {}
    set_config = app.upload_set_config
    defaults = dict(dest=app.config.get('UPLOADS_DEFAULT_DEST'),
                    url=app.config.get('UPLOADS_DEFAULT_URL'))

    for uset in upload_sets:
        config = config_for_set(uset, app, defaults)
        set_config[uset.name] = config

    should_serve = any(s.base_url is None for s in set_config.values())
    if '_uploads' not in app.blueprints and should_serve:
        app.register_blueprint(uploads_mod)

class All(object):
    """
    This type can be used to allow all extensions. There is a predefined
    instance named `ALL`.
    """
    def __contains__(self, item):
        return True

#: This "contains" all items. You can use it to allow all extensions to be
#: uploaded.
ALL = All()

class AllExcept(object):
    """
    This can be used to allow all file types except certain ones. For example,
    to ban .exe and .iso files, pass::

        AllExcept(('exe', 'iso'))

    to the `UploadSet` constructor as `extensions`. You can use any container,
    for example::

        AllExcept(SCRIPTS + EXECUTABLES)
    """
    def __init__(self, items):
        self.items = items

    def __contains__(self, item):
        return item not in self.items

class UploadConfiguration(object):
    """
    This holds the configuration for a single `UploadSet`. The constructor's
    arguments are also the attributes.

    :param destination: The directory to save files to.
    :param base_url: The URL (ending with a /) that files can be downloaded
                     from. If this is `None`, Flask-Uploads will serve the
                     files itself.
    :param allow: A list of extensions to allow, even if they're not in the
                  `UploadSet` extensions list.
    :param deny: A list of extensions to deny, even if they are in the
                 `UploadSet` extensions list.
    """
    def __init__(self, destination, base_url=None, allow=(), deny=()):
        self.destination = destination
        self.base_url = base_url
        self.allow = allow
        self.deny = deny

    @property
    def tuple(self):
        return (self.destination, self.base_url, self.allow, self.deny)

    def __eq__(self, other):
        return self.tuple == other.tuple

class UploadSet(object):
    """
    This represents a single set of uploaded files. Each upload set is
    independent of the others. This can be reused across multiple application
    instances, as all configuration is stored on the application object itself
    and found with `flask.current_app`.

    :param name: The name of this upload set. It defaults to ``files``, but
                 you can pick any alphanumeric name you want. (For simplicity,
                 it's best to use a plural noun.)
    :param extensions: The extensions to allow uploading in this set. The
                       easiest way to do this is to add together the extension
                       presets (for example, ``TEXT + DOCUMENTS + IMAGES``).
                       It can be overridden by the configuration with the
                       `UPLOADED_X_ALLOW` and `UPLOADED_X_DENY` configuration
                       parameters. The default is `DEFAULTS`.
    :param default_dest: If given, this should be a callable. If you call it
                         with the app, it should return the default upload
                         destination path for that app.
    """
    def __init__(self, name='files', extensions=DEFAULTS, default_dest=None):
        if not name.isalnum():
            raise ValueError("Name must be alphanumeric (no underscores)")
        self.name = name
        self.extensions = extensions
        self._config = None
        self.default_dest = default_dest

    @property
    def config(self):
        """
        This gets the current configuration. By default, it looks up the
        current application and gets the configuration from there. But if you
        don't want to go to the full effort of setting an application, or it's
        otherwise outside of a request context, set the `_config` attribute to
        an `UploadConfiguration` instance, then set it back to `None` when
        you're done.
        """
        if self._config is not None:
            return self._config
        try:
            return current_app.upload_set_config[self.name]
        except AttributeError:
            raise RuntimeError("cannot access configuration outside request")

    def url(self, filename):
        """
        This function gets the URL a file uploaded to this set would be
        accessed at. It doesn't check whether said file exists.

        :param filename: The filename to return the URL for.
        """
        base = self.config.base_url
        if base is None:
            return url_for('_uploads.uploaded_file', setname=self.name,
                           filename=filename, _external=True)
        else:
            return base + filename

    def path(self, filename, folder=None):
        """
        This returns the absolute path of a file uploaded to this set. It
        doesn't actually check whether said file exists.

        :param filename: The filename to return the path for.
        :param folder: The subfolder within the upload set previously used
                       to save to.
        """
        if folder is not None:
            target_folder = os.path.join(self.config.destination, folder)
        else:
            target_folder = self.config.destination
        return os.path.join(target_folder, filename)

    def file_allowed(self, storage, basename):
        """
        This tells whether a file is allowed. It should return `True` if the
        given `werkzeug.FileStorage` object can be saved with the given
        basename, and `False` if it can't. The default implementation just
        checks the extension, so you can override this if you want.

        :param storage: The `werkzeug.FileStorage` to check.
        :param basename: The basename it will be saved under.
        """
        return self.extension_allowed(extension(basename))

    def extension_allowed(self, ext):
        """
        This determines whether a specific extension is allowed. It is called
        by `file_allowed`, so if you override that but still want to check
        extensions, call back into this.

        :param ext: The extension to check, without the dot.
        """
        return ((ext in self.config.allow) or
                (ext in self.extensions and ext not in self.config.deny))

    def get_basename(self, filename):
        return lowercase_ext(secure_filename(filename))

    def save(self, storage, folder=None, name=None):
        """
        This saves a `werkzeug.FileStorage` into this upload set. If the
        upload is not allowed, an `UploadNotAllowed` error will be raised.
        Otherwise, the file will be saved and its name (including the folder)
        will be returned.

        :param storage: The uploaded file to save.
        :param folder: The subfolder within the upload set to save to.
        :param name: The name to save the file as. If it ends with a dot, the
                     file's extension will be appended to the end. (If you
                     are using `name`, you can include the folder in the
                     `name` instead of explicitly using `folder`, i.e.
                     ``uset.save(file, name="someguy/photo_123.")``
        """
        if not isinstance(storage, FileStorage):
            raise TypeError("storage must be a werkzeug.FileStorage")

        if folder is None and name is not None and "/" in name:
            folder, name = os.path.split(name)

        basename = self.get_basename(storage.filename)
        if name:
            if name.endswith('.'):
                basename = name + extension(basename)
            else:
                basename = name

        if not self.file_allowed(storage, basename):
            raise UploadNotAllowed()

        if folder:
            target_folder = os.path.join(self.config.destination, folder)
        else:
            target_folder = self.config.destination
        if not os.path.exists(target_folder):
            os.makedirs(target_folder)
        if os.path.exists(os.path.join(target_folder, basename)):
            basename = self.resolve_conflict(target_folder, basename)

        target = os.path.join(target_folder, basename)
        storage.save(target)
        if folder:
            return posixpath.join(folder, basename)
        else:
            return basename

    def resolve_conflict(self, target_folder, basename):
        """
        If a file with the selected name already exists in the target folder,
        this method is called to resolve the conflict. It should return a new
        basename for the file.

        The default implementation splits the name and extension and adds a
        suffix to the name consisting of an underscore and a number, and tries
        that until it finds one that doesn't exist.

        :param target_folder: The absolute path to the target.
        :param basename: The file's original basename.
        """
        name, ext = os.path.splitext(basename)
        count = 0
        while True:
            count = count + 1
            newname = '%s_%d%s' % (name, count, ext)
            if not os.path.exists(os.path.join(target_folder, newname)):
                return newname

uploads_mod = Blueprint('_uploads', __name__, url_prefix='/_uploads')

@uploads_mod.route('/<setname>/<path:filename>')
def uploaded_file(setname, filename):
    config = current_app.upload_set_config.get(setname)
    if config is None:
        abort(404)
    return send_from_directory(config.destination, filename)

class TestingFileStorage(FileStorage):
    """
    This is a helper for testing upload behavior in your application. You
    can manually create it, and its save method is overloaded to set `saved`
    to the name of the file it was saved to. All of these parameters are
    optional, so only bother setting the ones relevant to your application.

    :param stream: A stream. The default is an empty stream.
    :param filename: The filename uploaded from the client. The default is the
                     stream's name.
    :param name: The name of the form field it was loaded from. The default is
                 `None`.
    :param content_type: The content type it was uploaded as. The default is
                         ``application/octet-stream``.
    :param content_length: How long it is. The default is -1.
    :param headers: Multipart headers as a `werkzeug.Headers`. The default is
                    `None`.
    """
    def __init__(self, stream=None, filename=None, name=None,
                 content_type='application/octet-stream', content_length=-1,
                 headers=None):
        FileStorage.__init__(self, stream, filename, name=name,
            content_type=content_type, content_length=content_length,
            headers=None)
        self.saved = None

    def save(self, dst, buffer_size=16384):
        """
        This marks the file as saved by setting the `saved` attribute to the
        name of the file it was saved to.

        :param dst: The file to save to.
        :param buffer_size: Ignored.
        """
        if isinstance(dst, string_types):
            self.saved = dst
        else:
            self.saved = dst.name


"""Setup script for vprof."""
import glob
import re
import shlex
import subprocess
import unittest

import pkg_resources
from distutils import cmd
from setuptools import setup
from setuptools.command.install import install

class RunUnittestsCommand(cmd.Command):
    description = 'Run all unittests'
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        suite = unittest.TestLoader().discover(
            'vprof/tests/.', pattern="*_test.py")
        unittest.TextTestRunner(verbosity=2, buffer=True).run(suite)
        subprocess.check_call(shlex.split('npm run test'))

class RunEndToEndTestCommand(cmd.Command):
    description = 'Run all end to end tests'
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        suite = unittest.TestLoader().discover(
            'vprof/tests/.', pattern="*_e2e.py")
        unittest.TextTestRunner(verbosity=2, buffer=True).run(suite)

class RunLintCommand(cmd.Command):
    description = 'Run linter'
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        subprocess.check_call(shlex.split('npm run lint'))
        subprocess.check_call(shlex.split(
            'pylint --reports=n --rcfile=.pylintrc ' + ' '.join(
                glob.glob('vprof/*.py'))))
        subprocess.check_call(shlex.split(
            'pylint --reports=n --rcfile=.pylintrc ' + ' '.join(
                glob.glob('vprof/tests/*.py'))))

class RunCleanCommand(cmd.Command):
    description = 'Clean temporary files'
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        subprocess.check_output(
            shlex.split('rm -rf vprof/ui/vprof_min.js'))

class RunDepsInstallCommand(cmd.Command):
    description = 'Install dependencies'
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        subprocess.check_call(
            ["python3", '-m', 'pip', 'install', '-r', 'requirements.txt'])
        subprocess.check_call(
            ["python3", '-m', 'pip', 'install', '-r', 'dev_requirements.txt'])
        subprocess.check_call(shlex.split('npm install'))

class VProfBuild(install):

    def run(self):
        subprocess.check_call(shlex.split('npm run build'))

class VProfInstall(install):

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

def get_vprof_version(filename):
    """Returns actual version specified in filename."""
    with open(filename) as src_file:
        version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
                                  src_file.read(), re.M)
        if version_match:
            return version_match.group(1)
        raise RuntimeError('Unable to find version info.')

def get_description():
    """Reads README.md file."""
    with open('README.md') as readme_file:
        return readme_file.read()

def get_requirements():
    with open('requirements.txt') as fp:
        return [str(r) for r in pkg_resources.parse_requirements(fp)]

setup(
    name='vprof',
    version=get_vprof_version('vprof/__main__.py'),
    packages=['vprof'],
    description="Visual profiler for Python",
    url='http://github.com/nvdv/vprof',
    license='BSD',
    author='nvdv',
    author_email='aflatnine@gmail.com',
    include_package_data=True,
    keywords=['debugging', 'profiling'],
    entry_points={
        'console_scripts': [
            'vprof = vprof.__main__:main'
        ]
    },
    classifiers=[
        'Development Status :: 3 - Alpha',
        'Environment :: Web Environment',
        'Intended Audience :: Developers',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3',
        'Topic :: Software Development',
    ],
    install_requires=get_requirements(),
    long_description=get_description(),
    long_description_content_type="text/markdown",
    cmdclass={
        'test': RunUnittestsCommand,
        'e2e_test': RunEndToEndTestCommand,
        'lint': RunLintCommand,
        'deps_install': RunDepsInstallCommand,
        'build_ui': VProfBuild,
        'install': VProfInstall,
    },
)