import base64

import numpy as np
import os
import importlib
import sys

vtk_module_name = os.environ.get("VTK_MODULE_NAME", "vtkmodules")
sys.modules["vtk_module"] = importlib.import_module(vtk_module_name)

from vtk_module.util.numpy_support import vtk_to_numpy
from vtk_module.vtkFiltersGeometry import vtkDataSetSurfaceFilter


def mesh(dataset, field_to_keep=None, point_arrays=None, cell_arrays=None):
    """Expect any dataset and extract its surface into a dash_vtk.Mesh state property"""
    if dataset is None:
        return None

    # Make sure we have a polydata to export
    polydata = None
    if dataset.IsA("vtkPolyData"):
        polydata = dataset
    else:
        extract_skin_filter = vtkDataSetSurfaceFilter()
        extract_skin_filter.SetInputData(dataset)
        extract_skin_filter.Update()
        polydata = extract_skin_filter.GetOutput()

    if polydata.GetPoints() is None:
        return None

    # Extract mesh
    state = {"mesh": {}}

    points = mesh_array(polydata.GetPoints())
    if points:
        state["mesh"]["points"] = points

    verts = mesh_array(polydata.GetVerts())
    if verts:
        state["mesh"]["verts"] = verts

    lines = mesh_array(polydata.GetLines())
    if lines:
        state["mesh"]["lines"] = lines

    polys = mesh_array(polydata.GetPolys())
    if polys:
        state["mesh"]["polys"] = polys

    strips = mesh_array(polydata.GetStrips())
    if strips:
        state["mesh"]["strips"] = strips

    # Scalars
    if field_to_keep is not None:
        field = None
        p_array = polydata.GetPointData().GetArray(field_to_keep)
        c_array = polydata.GetCellData().GetArray(field_to_keep)

        if c_array:
            field = data_array(c_array, location="CellData", name=field_to_keep)

        if p_array:
            field = data_array(p_array, location="PointData", name=field_to_keep)

        if field:
            state.update({"field": field})

    # PointData Fields
    if point_arrays:
        point_data = field_data(polydata.GetPointData(), point_arrays, "PointData")
        if len(point_data):
            state.update({"pointArrays": point_data})

    # CellData Fields
    if cell_arrays:
        cell_data = field_data(polydata.GetCellData(), cell_arrays, "CellData")
        if len(cell_data):
            state.update({"cellArrays": cell_data})

    return state


def mesh_array(array):
    if array:
        return b64_encode_numpy(vtk_to_numpy(array.GetData()))


def data_array(data_array, location="PointData", name=None):
    if data_array:
        data_range = data_array.GetRange(-1)
        nb_comp = data_array.GetNumberOfComponents()
        values = vtk_to_numpy(data_array)
        js_types = to_js_type[str(values.dtype)]
        return {
            "name": name if name else data_array.GetName(),
            "values": b64_encode_numpy(values),
            "numberOfComponents": nb_comp,
            "type": js_types,
            "location": location,
            "dataRange": data_range,
        }


def field_data(field_data, names, location="PointData"):
    fields = []
    for name in names:
        array = field_data.GetArray(name)
        js_array = data_array(array, location, name)
        if js_array:
            fields.append(js_array)

    return fields


def volume(dataset):
    """Expect a vtkImageData and extract its setting for the dash_vtk.Volume state"""
    if dataset is None or not dataset.IsA("vtkImageData"):
        return None

    state = {
        "image": {
            "dimensions": dataset.GetDimensions(),
            "spacing": dataset.GetSpacing(),
            "origin": dataset.GetOrigin(),
        },
    }

    # Capture image orientation if any
    if hasattr(dataset, "GetDirectionMatrix"):
        matrix = dataset.GetDirectionMatrix()
        js_mat = []
        for j in range(3):
            for i in range(3):
                js_mat.append(matrix.GetElement(i, j))

        state["image"]["direction"] = js_mat

    scalars = dataset.GetPointData().GetScalars()
    field = data_array(scalars, location="PointData")
    if field:
        state["field"] = field

    return state


# Numpy to JS TypedArray
to_js_type = {
    "int8": "Int8Array",
    "uint8": "Uint8Array",
    "int16": "Int16Array",
    "uint16": "Uint16Array",
    "int32": "Int32Array",
    "uint32": "Uint32Array",
    "int64": "Int32Array",
    "uint64": "Uint32Array",
    "float32": "Float32Array",
    "float64": "Float64Array",
}


def b64_encode_numpy(obj):
    # Convert 1_d numpy arrays with numeric types to memoryviews with
    # datatype and shape metadata.
    if len(obj) == 0:
        return obj.tolist()

    dtype = obj.dtype
    if dtype.kind == "f":
        return np_encode(obj)
    elif dtype.kind == "b":
        return np_encode(obj, np.uint8)
    elif dtype.kind in ["u", "i"]:
        # Try to see if we can downsize the array
        max_value = np.amax(obj)
        min_value = np.amin(obj)
        signed = min_value < 0
        test_value = max(max_value, -min_value)
        if signed:
            if test_value < np.iinfo(np.int8):
                return np_encode(obj, np.int8)
            if test_value < np.iinfo(np.int16).max:
                return np_encode(obj, np.int16)
            if test_value < np.iinfo(np.int32).max:
                return np_encode(obj, np.int32)
        else:
            if test_value < np.iinfo(np.uint8).max:
                return np_encode(obj, np.uint8)
            if test_value < np.iinfo(np.uint16).max:
                return np_encode(obj, np.uint16)
            if test_value < np.iinfo(np.uint32).max:
                return np_encode(obj, np.uint32)

    # Convert all other numpy arrays to lists
    return obj.tolist()


def np_encode(array, np_type=None):
    if np_type:
        n_array = array.astype(np_type).ravel(order="C")
        return {
            "bvals": base64.b64encode(memoryview(n_array)).decode("utf-8"),
            "dtype": str(n_array.dtype),
            "shape": list(array.shape),
        }
    return {
        "bvals": base64.b64encode(memoryview(array.ravel(order="C"))).decode("utf-8"),
        "dtype": str(array.dtype),
        "shape": list(array.shape),
    }
