"""
.. module:: exchange_helpers
    :platform: Unix, Windows
    :synopsis: CAD exchange and interoperability module for NURBS-Python

.. moduleauthor:: Onur Rauf Bingol <orbingol@gmail.com>

"""

import warnings
import struct

from . import Abstract
from . import Multi
from . import exchange_helpers as exh


def save_obj(surf_in=None, file_name=None, vertex_spacing=2):
    """ Exports surface(s) as a .obj file.

    This function works as a router function between possible exporting options.

    :param surf_in: surface or surfaces to be saved
    :type surf_in: Abstract.Surface or Multi.MultiSurface
    :param file_name: name of the output file
    :type file_name: str
    :param vertex_spacing: size of the triangle edge in terms of points sampled on the surface
    :type vertex_spacing: int
    """
    if isinstance(surf_in, Multi.MultiSurface):
        save_obj_multi(surf_in, file_name, vertex_spacing)
    else:
        save_obj_single(surf_in, file_name, vertex_spacing)


# Saves B-Spline and/or NURBS surface as a Wavefront OBJ file
def save_obj_single(surface=None, file_name=None, vertex_spacing=2):
    """ Saves a single surface as a .obj file.

    :param surface: surface to be saved
    :type surface: Abstract.Surface
    :param file_name: name of the output file
    :type file_name: str
    :param vertex_spacing: size of the triangle edge in terms of points sampled on the surface
    :type vertex_spacing: int
    """
    # Input validity checking
    if not isinstance(surface, Abstract.Surface):
        raise ValueError("Input is not a surface")
    if not file_name:
        raise ValueError("File name field is required")
    if vertex_spacing < 1 or not isinstance(vertex_spacing, int):
        raise ValueError("Vertex spacing must be an integer value and it must be bigger than zero")

    # Create the file and start saving triangulated surface points
    try:
        with open(file_name, 'w') as fp:
            fp.write("# Generated by NURBS-Python\n")
            vertices, triangles = exh.make_obj_triangles(surface.surfpts,
                                                         int((1.0 / surface.delta) + 1),
                                                         int((1.0 / surface.delta) + 1),
                                                         vertex_spacing)

            # Write vertices
            for vert_row in vertices:
                for vert in vert_row:
                    line = "v " + str(vert.x) + " " + str(vert.y) + " " + str(vert.z) + "\n"
                    fp.write(line)

            # Write vertex normals
            for vert_row in vertices:
                for vert in vert_row:
                    sn = surface.normal(vert.uv[0], vert.uv[1], True)
                    line = "vn " + str(sn[1][0]) + " " + str(sn[1][1]) + " " + str(sn[1][2]) + "\n"
                    fp.write(line)

            # Write faces
            for t in triangles:
                vl = t.vertex_ids
                line = "f " + str(vl[0]) + " " + str(vl[1]) + " " + str(vl[2]) + "\n"
                fp.write(line)
    except IOError:
        print("Cannot open " + str(file_name) + " for writing")


# Saves B-Spline and/or NURBS surfaces as a single Wavefront OBJ file
def save_obj_multi(surface_list=(), file_name=None, vertex_spacing=2):
    """ Saves multiple surfaces as a single .obj file.

    :param surface_list: list of surfaces to be saved
    :type surface_list: Multi.MultiSurface
    :param file_name: name of the output file
    :type file_name: str
    :param vertex_spacing: size of the triangle edge in terms of points sampled on the surface
    :type vertex_spacing: int
    """
    # Input validity checking
    if not isinstance(surface_list, Multi.MultiSurface):
        raise ValueError("Input must be a list of surfaces")
    if not file_name:
        raise ValueError("File name field is required")
    if vertex_spacing < 1 or not isinstance(vertex_spacing, int):
        raise ValueError("Vertex spacing must be an integer value and it must be bigger than zero")

    # Create the file and start saving triangulated surface points
    try:
        with open(file_name, 'w') as fp:
            fp.write("# Generated by NURBS-Python\n")
            vertex_offset = 0  # count the vertices to update the face numbers correctly

            # Initialize lists for vertices, vertex normals and faces
            str_v = []
            str_vn = []
            str_f = []

            # Loop through MultiSurface object
            for surface in surface_list:
                if not isinstance(surface, Abstract.Surface):
                    warnings.warn("Encountered a non-surface object")
                    continue

                # Set surface delta
                surface.delta = surface_list.delta

                # Generate triangles
                vertices, triangles = exh.make_obj_triangles(surface.surfpts,
                                                             int((1.0 / surface.delta) + 1),
                                                             int((1.0 / surface.delta) + 1),
                                                             vertex_spacing)

                # Collect vertices
                for vert_row in vertices:
                    for vert in vert_row:
                        line = "v " + str(vert.x) + " " + str(vert.y) + " " + str(vert.z) + "\n"
                        str_v.append(line)

                # Collect vertex normals
                for vert_row in vertices:
                    for vert in vert_row:
                        sn = surface.normal(vert.uv[0], vert.uv[1], True)
                        line = "vn " + str(sn[1][0]) + " " + str(sn[1][1]) + " " + str(sn[1][2]) + "\n"
                        str_vn.append(line)

                # Collect faces
                for t in triangles:
                    vl = t.vertex_ids
                    line = "f " + \
                           str(vl[0] + vertex_offset) + " " + \
                           str(vl[1] + vertex_offset) + " " + \
                           str(vl[2] + vertex_offset) + "\n"
                    str_f.append(line)

                # Update vertex offset
                vertex_offset = len(str_v)

            # Write all collected data to the file
            for line in str_v:
                fp.write(line)
            for line in str_vn:
                fp.write(line)
            for line in str_f:
                fp.write(line)
    except IOError:
        print("Cannot open " + str(file_name) + " for writing")


def save_stl(surf_in=None, file_name=None, binary=True, vertex_spacing=2):
    """ Exports surface(s) as a .stl file in plain text or binary format.

    This function works as a router function between possible exporting options.

    :param surf_in: surface or surfaces to be saved
    :type surf_in: Abstract.Surface or Multi.MultiSurface
    :param file_name: name of the output file
    :type file_name: str
    :param binary: True if the saved STL file is going to be in binary format
    :type binary: bool
    :param vertex_spacing: size of the triangle edge in terms of points sampled on the surface
    :type vertex_spacing: int
    """
    if isinstance(surf_in, Multi.MultiSurface):
        if binary:
            save_stl_binary_multi(surf_in, file_name, vertex_spacing)
        else:
            save_stl_ascii_multi(surf_in, file_name, vertex_spacing)
    else:
        if binary:
            save_stl_binary_single(surf_in, file_name, vertex_spacing)
        else:
            save_stl_ascii_single(surf_in, file_name, vertex_spacing)


def save_stl_ascii_single(surface=None, file_name=None, vertex_spacing=2):
    """ Saves a single surface as an ASCII .stl file.

    :param surface: surface to be saved
    :type surface: Abstract.Surface
    :param file_name: name of the output file
    :type file_name: str
    :param vertex_spacing: size of the triangle edge in terms of points sampled on the surface
    :type vertex_spacing: int
    """
    # Input validity checking
    if not isinstance(surface, Abstract.Surface):
        raise ValueError("Input is not a surface")
    if not file_name:
        raise ValueError("File name field is required")
    if vertex_spacing < 1 or not isinstance(vertex_spacing, int):
        raise ValueError("Vertex spacing must be an integer value and it must be bigger than zero")

    # Create the file and start saving triangulated surface points
    try:
        with open(file_name, 'w') as fp:
            vertices, triangles = exh.make_obj_triangles(surface.surfpts,
                                                         int((1.0 / surface.delta) + 1),
                                                         int((1.0 / surface.delta) + 1),
                                                         vertex_spacing)

            fp.write("solid Surface\n")
            for t in triangles:
                line = "\tfacet normal " + str(t.normal[0]) + " " + str(t.normal[1]) + " " + str(t.normal[2]) + "\n"
                fp.write(line)
                fp.write("\t\touter loop\n")
                for v in t.vertices:
                    line = "\t\t\tvertex " + str(v.x) + " " + str(v.y) + " " + str(v.z) + "\n"
                    fp.write(line)
                fp.write("\t\tendloop\n")
                fp.write("\tendfacet\n")
            fp.write("endsolid Surface\n")
    except IOError:
        print("Cannot open " + str(file_name) + " for writing")


def save_stl_ascii_multi(surface_list=(), file_name=None, vertex_spacing=2):
    """ Saves multiple surfaces as an ASCII .stl file.

    :param surface_list: list of surfaces to be saved
    :type surface_list: Multi.MultiAbstract
    :param file_name: name of the output file
    :type file_name: str
    :param vertex_spacing: size of the triangle edge in terms of points sampled on the surface
    :type vertex_spacing: int
    """
    # Input validity checking
    if not isinstance(surface_list, Abstract.Multi):
        raise ValueError("Input must be a list of surfaces")
    if not file_name:
        raise ValueError("File name field is required")
    if vertex_spacing < 1 or not isinstance(vertex_spacing, int):
        raise ValueError("Vertex spacing must be an integer value and it must be bigger than zero")

    # Create the file and start saving triangulated surface points
    try:
        with open(file_name, 'w') as fp:
            fp.write("solid Surface\n")

            # Loop through MultiSurface object
            for surface in surface_list:
                if not isinstance(surface, Abstract.Surface):
                    warnings.warn("Encountered a non-surface object")
                    continue

                # Set surface delta
                surface.delta = surface_list.delta

                vertices, triangles = exh.make_obj_triangles(surface.surfpts,
                                                             int((1.0 / surface.delta) + 1),
                                                             int((1.0 / surface.delta) + 1),
                                                             vertex_spacing)
                for t in triangles:
                    line = "\tfacet normal " + str(t.normal[0]) + " " + str(t.normal[1]) + " " + str(t.normal[2]) + "\n"
                    fp.write(line)
                    fp.write("\t\touter loop\n")
                    for v in t.vertices:
                        line = "\t\t\tvertex " + str(v.x) + " " + str(v.y) + " " + str(v.z) + "\n"
                        fp.write(line)
                    fp.write("\t\tendloop\n")
                    fp.write("\tendfacet\n")

            fp.write("endsolid Surface\n")
    except IOError:
        print("Cannot open " + str(file_name) + " for writing")


def save_stl_binary_single(surface=None, file_name=None, vertex_spacing=2):
    """ Saves a single surface as a binary .stl file.

    Inspired from https://github.com/apparentlymart/python-stl

    :param surface: surface to be saved
    :type surface: Abstract.Surface
    :param file_name: name of the output file
    :type file_name: str
    :param vertex_spacing: size of the triangle edge in terms of points sampled on the surface
    :type vertex_spacing: int
    """
    # Input validity checking
    if not isinstance(surface, Abstract.Surface):
        raise ValueError("Input is not a surface")
    if not file_name:
        raise ValueError("File name field is required")
    if vertex_spacing < 1 or not isinstance(vertex_spacing, int):
        raise ValueError("Vertex spacing must be an integer value and it must be bigger than zero")

    # Create the file and start saving triangulated surface points
    try:
        with open(file_name, 'wb') as fp:
            vertices, triangles = exh.make_obj_triangles(surface.surfpts,
                                                         int((1.0 / surface.delta) + 1),
                                                         int((1.0 / surface.delta) + 1),
                                                         vertex_spacing)

            # Write triangle list to the binary STL file
            fp.write(b'\0' * 80)  # header
            fp.write(struct.pack('<i', len(triangles)))  # number of triangles
            for t in triangles:
                fp.write(struct.pack('<3f', *t.normal))  # normal
                for v in t.vertices:
                    fp.write(struct.pack('<3f', *v.data))  # vertices
                fp.write(b'\0\0')  # attribute byte count
    except IOError:
        print("Cannot open " + str(file_name) + " for writing")


def save_stl_binary_multi(surface_list=(), file_name=None, vertex_spacing=2):
    """ Saves multiple surfaces as a binary .stl file.

    :param surface_list: list of surfaces to be saved
    :type surface_list: Multi.MultiAbstract
    :param file_name: name of the output file
    :type file_name: str
    :param vertex_spacing: size of the triangle edge in terms of points sampled on the surface
    :type vertex_spacing: int
    """
    # Input validity checking
    if not isinstance(surface_list, Abstract.Multi):
        raise ValueError("Input must be a list of surfaces")
    if not file_name:
        raise ValueError("File name field is required")
    if vertex_spacing < 1 or not isinstance(vertex_spacing, int):
        raise ValueError("Vertex spacing must be an integer value and it must be bigger than zero")

    # Create the file and start saving triangulated surface points
    try:
        with open(file_name, 'wb') as fp:
            # Loop through MultiSurface object
            triangles_list = []
            for surface in surface_list:
                if not isinstance(surface, Abstract.Surface):
                    warnings.warn("Encountered a non-surface object")
                    continue

                # Set surface delta
                surface.delta = surface_list.delta

                vertices, triangles = exh.make_obj_triangles(surface.surfpts,
                                                             int((1.0 / surface.delta) + 1),
                                                             int((1.0 / surface.delta) + 1),
                                                             vertex_spacing)
                triangles_list += triangles

            # Write triangle list to the binary STL file
            fp.write(b'\0' * 80)  # header
            fp.write(struct.pack('<i', len(triangles_list)))  # number of triangles
            for t in triangles_list:
                fp.write(struct.pack('<3f', *t.normal))  # normal
                for v in t.vertices:
                    fp.write(struct.pack('<3f', *v.data))  # vertices
                fp.write(b'\0\0')  # attribute byte count
    except IOError:
        print("Cannot open " + str(file_name) + " for writing")
