# Copyright 2018-2019 The glTF-Blender-IO authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import typing
import math
from mathutils import Matrix, Vector, Quaternion, Euler

from io_scene_gltf2.blender.com.gltf2_blender_data_path import get_target_property_name


def list_to_mathutils(values: typing.List[float], data_path: str) -> typing.Union[Vector, Quaternion, Euler]:
    """Transform a list to blender py object."""
    target = get_target_property_name(data_path)

    if target == 'delta_location':
        return Vector(values)  # TODO Should be Vector(values) - Vector(something)?
    elif target == 'delta_rotation_euler':
        return Euler(values).to_quaternion()  # TODO Should be Euler(values).to_quaternion() @ something?
    elif target == 'location':
        return Vector(values)
    elif target == 'rotation_axis_angle':
        angle = values[0]
        axis = values[1:]
        return Quaternion(axis, math.radians(angle))
    elif target == 'rotation_euler':
        return Euler(values).to_quaternion()
    elif target == 'rotation_quaternion':
        return Quaternion(values)
    elif target == 'scale':
        return Vector(values)
    elif target == 'value':
        return Vector(values)

    return values


def mathutils_to_gltf(x: typing.Union[Vector, Quaternion]) -> typing.List[float]:
    """Transform a py object to glTF list."""
    if isinstance(x, Vector):
        return list(x)
    if isinstance(x, Quaternion):
        # Blender has w-first quaternion notation
        return [x[1], x[2], x[3], x[0]]
    else:
        return list(x)


def to_yup() -> Matrix:
    """Transform to Yup."""
    return Matrix(
        ((1.0, 0.0, 0.0, 0.0),
         (0.0, 0.0, 1.0, 0.0),
         (0.0, -1.0, 0.0, 0.0),
         (0.0, 0.0, 0.0, 1.0))
    )


to_zup = to_yup


def swizzle_yup(v: typing.Union[Vector, Quaternion], data_path: str) -> typing.Union[Vector, Quaternion]:
    """Manage Yup."""
    target = get_target_property_name(data_path)
    swizzle_func = {
        "delta_location": swizzle_yup_location,
        "delta_rotation_euler": swizzle_yup_rotation,
        "location": swizzle_yup_location,
        "rotation_axis_angle": swizzle_yup_rotation,
        "rotation_euler": swizzle_yup_rotation,
        "rotation_quaternion": swizzle_yup_rotation,
        "scale": swizzle_yup_scale,
        "value": swizzle_yup_value
    }.get(target)

    if swizzle_func is None:
        raise RuntimeError("Cannot transform values at {}".format(data_path))

    return swizzle_func(v)


def swizzle_yup_location(loc: Vector) -> Vector:
    """Manage Yup location."""
    return Vector((loc[0], loc[2], -loc[1]))


def swizzle_yup_rotation(rot: Quaternion) -> Quaternion:
    """Manage Yup rotation."""
    return Quaternion((rot[0], rot[1], rot[3], -rot[2]))


def swizzle_yup_scale(scale: Vector) -> Vector:
    """Manage Yup scale."""
    return Vector((scale[0], scale[2], scale[1]))


def swizzle_yup_value(value: typing.Any) -> typing.Any:
    """Manage Yup value."""
    return value


def transform(v: typing.Union[Vector, Quaternion], data_path: str, transform: Matrix = Matrix.Identity(4)) -> typing \
        .Union[Vector, Quaternion]:
    """Manage transformations."""
    target = get_target_property_name(data_path)
    transform_func = {
        "delta_location": transform_location,
        "delta_rotation_euler": transform_rotation,
        "location": transform_location,
        "rotation_axis_angle": transform_rotation,
        "rotation_euler": transform_rotation,
        "rotation_quaternion": transform_rotation,
        "scale": transform_scale,
        "value": transform_value
    }.get(target)

    if transform_func is None:
        raise RuntimeError("Cannot transform values at {}".format(data_path))

    return transform_func(v, transform)


def transform_location(location: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector:
    """Transform location."""
    m = Matrix.Translation(location)
    m = transform @ m
    return m.to_translation()


def transform_rotation(rotation: Quaternion, transform: Matrix = Matrix.Identity(4)) -> Quaternion:
    """Transform rotation."""
    rotation.normalize()
    m = rotation.to_matrix().to_4x4()
    m = transform @ m
    return m.to_quaternion()


def transform_scale(scale: Vector, transform: Matrix = Matrix.Identity(4)) -> Vector:
    """Transform scale."""
    m = Matrix.Identity(4)
    m[0][0] = scale.x
    m[1][1] = scale.y
    m[2][2] = scale.z
    m = transform @ m

    return m.to_scale()


def transform_value(value: Vector, _: Matrix = Matrix.Identity(4)) -> Vector:
    """Transform value."""
    return value


def round_if_near(value: float, target: float) -> float:
    """If value is very close to target, round to target."""
    return value if abs(value - target) > 2.0e-6 else target


def scale_rot_swap_matrix(rot):
    """Returns a matrix m st. Scale[s] Rot[rot] = Rot[rot] Scale[m s].
    If rot.to_matrix() is a signed permutation matrix, works for any s.
    Otherwise works only if s is a uniform scaling.
    """
    m = nearby_signed_perm_matrix(rot)  # snap to signed perm matrix
    m.transpose()  # invert permutation
    for i in range(3):
        for j in range(3):
            m[i][j] = abs(m[i][j])  # discard sign
    return m


def nearby_signed_perm_matrix(rot):
    """Returns a signed permutation matrix close to rot.to_matrix().
    (A signed permutation matrix is like a permutation matrix, except
    the non-zero entries can be ±1.)
    """
    m = rot.to_matrix()
    x, y, z = m[0], m[1], m[2]

    # Set the largest entry in the first row to ±1
    a, b, c = abs(x[0]), abs(x[1]), abs(x[2])
    i = 0 if a >= b and a >= c else 1 if b >= c else 2
    x[i] = 1 if x[i] > 0 else -1
    x[(i+1) % 3] = 0
    x[(i+2) % 3] = 0

    # Same for second row: only two columns to consider now.
    a, b = abs(y[(i+1) % 3]), abs(y[(i+2) % 3])
    j = (i+1) % 3 if a >= b else (i+2) % 3
    y[j] = 1 if y[j] > 0 else -1
    y[(j+1) % 3] = 0
    y[(j+2) % 3] = 0

    # Same for third row: only one column left
    k = (0 + 1 + 2) - i - j
    z[k] = 1 if z[k] > 0 else -1
    z[(k+1) % 3] = 0
    z[(k+2) % 3] = 0

    return m
