"""
This code is modified from:
http://theorangeduck.com/page/deep-learning-framework-character-motion-synthesis-and-editing

by Daniel Holden et al
"""


import re
import numpy as np
from ..geometries.quaternion import Quaternions
import torch
from ..geometry import quat2euler, quat2repr6d, repr6d2quat
from alfred import logger


channelmap = {"Xrotation": "x", "Yrotation": "y", "Zrotation": "z"}

channelmap_inv = {
    "x": "Xrotation",
    "y": "Yrotation",
    "z": "Zrotation",
}

ordermap = {
    "x": 0,
    "y": 1,
    "z": 2,
}


class Animation:
    def __init__(
        self, rotations, positions, orients, offsets, parents, names, frametime
    ):
        self.rotations = rotations
        self.positions = positions
        self.orients = orients
        self.offsets = offsets
        self.parent = parents
        self.names = names
        self.frametime = frametime

    @property
    def shape(self):
        return self.rotations.shape


def load(
    filename, start=None, end=None, order=None, world=False, need_quater=False
) -> Animation:
    """
    Reads a BVH file and constructs an animation

    Parameters
    ----------
    filename: str
        File to be opened

    start : int
        Optional Starting Frame

    end : int
        Optional Ending Frame

    order : str
        Optional Specifier for joint order.
        Given as string E.G 'xyz', 'zxy'

    world : bool
        If set to true euler angles are applied
        together in world space rather than local
        space
    Returns
    -------

    (animation, joint_names, frametime)
        Tuple of loaded animation and joint names
    """

    f = open(filename, "r")

    i = 0
    active = -1
    end_site = False

    names = []
    orients = Quaternions.id(0)
    offsets = np.array([]).reshape((0, 3))
    parents = np.array([], dtype=int)
    orders = []

    for line in f:

        if "HIERARCHY" in line:
            continue
        if "MOTION" in line:
            continue

        """ Modified line read to handle mixamo data """
        #        rmatch = re.match(r"ROOT (\w+)", line)
        rmatch = re.match(r"ROOT (\w+:?\w+)", line)
        if rmatch:
            names.append(rmatch.group(1))
            offsets = np.append(offsets, np.array([[0, 0, 0]]), axis=0)
            orients.qs = np.append(orients.qs, np.array([[1, 0, 0, 0]]), axis=0)
            parents = np.append(parents, active)
            active = len(parents) - 1
            continue

        if "{" in line:
            continue

        if "}" in line:
            if end_site:
                end_site = False
            else:
                active = parents[active]
            continue

        offmatch = re.match(
            r"\s*OFFSET\s+([\-\d\.e]+)\s+([\-\d\.e]+)\s+([\-\d\.e]+)", line
        )
        if offmatch:
            if not end_site:
                offsets[active] = np.array([list(map(float, offmatch.groups()))])
            continue

        chanmatch = re.match(r"\s*CHANNELS\s+(\d+)", line)
        if chanmatch:
            channels = int(chanmatch.group(1))

            channelis = 0 if channels == 3 else 3
            channelie = 3 if channels == 3 else 6
            parts = line.split()[2 + channelis : 2 + channelie]
            if any([p not in channelmap for p in parts]):
                continue
            order = "".join([channelmap[p] for p in parts])
            orders.append(order)
            continue

        """ Modified line read to handle mixamo data """
        #        jmatch = re.match("\s*JOINT\s+(\w+)", line)
        jmatch = re.match("\s*JOINT\s+(\w+:?\w+)", line)
        if jmatch:
            names.append(jmatch.group(1))
            offsets = np.append(offsets, np.array([[0, 0, 0]]), axis=0)
            orients.qs = np.append(orients.qs, np.array([[1, 0, 0, 0]]), axis=0)
            parents = np.append(parents, active)
            active = len(parents) - 1
            continue

        if "End Site" in line:
            end_site = True
            continue

        fmatch = re.match("\s*Frames:\s+(\d+)", line)
        if fmatch:
            if start and end:
                fnum = (end - start) - 1
            else:
                fnum = int(fmatch.group(1))
            jnum = len(parents)
            positions = offsets[np.newaxis].repeat(fnum, axis=0)
            rotations = np.zeros((fnum, len(orients), 3))
            continue

        fmatch = re.match("\s*Frame Time:\s+([\d\.]+)", line)
        if fmatch:
            frametime = float(fmatch.group(1))
            continue

        if (start and end) and (i < start or i >= end - 1):
            i += 1
            continue

        # dmatch = line.strip().split(' ')
        dmatch = line.strip().split()
        if dmatch:
            data_block = np.array(list(map(float, dmatch)))
            N = len(parents)
            fi = i - start if start else i
            if channels == 3:
                positions[fi, 0:1] = data_block[0:3]
                rotations[fi, :] = data_block[3:].reshape(N, 3)
            elif channels == 6:
                data_block = data_block.reshape(N, 6)
                positions[fi, :] = data_block[:, 0:3]
                rotations[fi, :] = data_block[:, 3:6]
            elif channels == 9:
                positions[fi, 0] = data_block[0:3]
                data_block = data_block[3:].reshape(N - 1, 9)
                rotations[fi, 1:] = data_block[:, 3:6]
                positions[fi, 1:] += data_block[:, 0:3] * data_block[:, 6:9]
            else:
                raise Exception("Too many channels! %i" % channels)

            i += 1

    f.close()

    all_rotations = []
    canonical_order = "xyz"
    for i, order in enumerate(orders):
        rot = rotations[:, i : i + 1]
        if need_quater:
            quat = Quaternions.from_euler(np.radians(rot), order=order, world=world)
            all_rotations.append(quat)
            continue
        elif order != canonical_order:
            quat = Quaternions.from_euler(np.radians(rot), order=order, world=world)
            rot = np.degrees(quat.euler(order=canonical_order))
        all_rotations.append(rot)
    rotations = np.concatenate(all_rotations, axis=1)

    return Animation(rotations, positions, orients, offsets, parents, names, frametime)


def save(
    filename,
    anim,
    names=None,
    frametime=1.0 / 24.0,
    order="zyx",
    positions=False,
    orients=True,
):
    """
    Saves an Animation to file as BVH

    Parameters
    ----------
    filename: str
        File to be saved to

    anim : Animation
        Animation to save

    names : [str]
        List of joint names

    order : str
        Optional Specifier for joint order.
        Given as string E.G 'xyz', 'zxy'

    frametime : float
        Optional Animation Frame time

    positions : bool
        Optional specfier to save bone
        positions for each frame

    orients : bool
        Multiply joint orients to the rotations
        before saving.

    """

    if names is None:
        names = ["joint_" + str(i) for i in range(len(anim.parents))]

    with open(filename, "w") as f:

        t = ""
        f.write("%sHIERARCHY\n" % t)
        f.write("%sROOT %s\n" % (t, names[0]))
        f.write("%s{\n" % t)
        t += "\t"

        f.write(
            "%sOFFSET %f %f %f\n"
            % (t, anim.offsets[0, 0], anim.offsets[0, 1], anim.offsets[0, 2])
        )
        f.write(
            "%sCHANNELS 6 Xposition Yposition Zposition %s %s %s \n"
            % (
                t,
                channelmap_inv[order[0]],
                channelmap_inv[order[1]],
                channelmap_inv[order[2]],
            )
        )

        for i in range(anim.shape[1]):
            if anim.parents[i] == 0:
                t = save_joint(f, anim, names, t, i, order=order, positions=positions)

        t = t[:-1]
        f.write("%s}\n" % t)

        f.write("MOTION\n")
        f.write("Frames: %i\n" % anim.shape[0])
        f.write("Frame Time: %f\n" % frametime)

        # if orients:
        #    rots = np.degrees((-anim.orients[np.newaxis] * anim.rotations).euler(order=order[::-1]))
        # else:
        #    rots = np.degrees(anim.rotations.euler(order=order[::-1]))
        rots = np.degrees(anim.rotations.euler(order=order[::-1]))
        poss = anim.positions

        for i in range(anim.shape[0]):
            for j in range(anim.shape[1]):

                if positions or j == 0:

                    f.write(
                        "%f %f %f %f %f %f "
                        % (
                            poss[i, j, 0],
                            poss[i, j, 1],
                            poss[i, j, 2],
                            rots[i, j, ordermap[order[0]]],
                            rots[i, j, ordermap[order[1]]],
                            rots[i, j, ordermap[order[2]]],
                        )
                    )

                else:

                    f.write(
                        "%f %f %f "
                        % (
                            rots[i, j, ordermap[order[0]]],
                            rots[i, j, ordermap[order[1]]],
                            rots[i, j, ordermap[order[2]]],
                        )
                    )

            f.write("\n")


def save_joint(f, anim, names, t, i, order="zyx", positions=False):

    f.write("%sJOINT %s\n" % (t, names[i]))
    f.write("%s{\n" % t)
    t += "\t"

    f.write(
        "%sOFFSET %f %f %f\n"
        % (t, anim.offsets[i, 0], anim.offsets[i, 1], anim.offsets[i, 2])
    )

    if positions:
        f.write(
            "%sCHANNELS 6 Xposition Yposition Zposition %s %s %s \n"
            % (
                t,
                channelmap_inv[order[0]],
                channelmap_inv[order[1]],
                channelmap_inv[order[2]],
            )
        )
    else:
        f.write(
            "%sCHANNELS 3 %s %s %s\n"
            % (
                t,
                channelmap_inv[order[0]],
                channelmap_inv[order[1]],
                channelmap_inv[order[2]],
            )
        )

    end_site = True

    for j in range(anim.shape[1]):
        if anim.parents[j] == i:
            t = save_joint(f, anim, names, t, j, order=order, positions=positions)
            end_site = False

    if end_site:
        f.write("%sEnd Site\n" % t)
        f.write("%s{\n" % t)
        t += "\t"
        f.write("%sOFFSET %f %f %f\n" % (t, 0.0, 0.0, 0.0))
        t = t[:-1]
        f.write("%s}\n" % t)

    t = t[:-1]
    f.write("%s}\n" % t)

    return t



# rotation with shape frame * J * 3
def write_bvh(parent, offset, rotation, position, names, frametime, order, path, endsite=None):
    logger.info('start write BVH file:')
    logger.info(f'names: {names}')
    logger.info(f'frametime: {frametime}')
    logger.info(f'path: {path}')

    file = open(path, 'w')
    frame = rotation.shape[0]
    joint_num = rotation.shape[1]
    order = order.upper()

    file_string = 'HIERARCHY\n'

    seq = []

    def write_static(idx, prefix):
        nonlocal parent, offset, rotation, names, order, endsite, file_string, seq
        seq.append(idx)
        if idx == 0:
            name_label = 'ROOT ' + names[idx]
            channel_label = 'CHANNELS 6 Xposition Yposition Zposition {}rotation {}rotation {}rotation'.format(*order)
        else:
            name_label = 'JOINT ' + names[idx]
            channel_label = 'CHANNELS 3 {}rotation {}rotation {}rotation'.format(*order)
        offset_label = 'OFFSET %.6f %.6f %.6f' % (offset[idx][0], offset[idx][1], offset[idx][2])

        file_string += prefix + name_label + '\n'
        file_string += prefix + '{\n'
        file_string += prefix + '\t' + offset_label + '\n'
        file_string += prefix + '\t' + channel_label + '\n'

        has_child = False
        for y in range(idx+1, rotation.shape[1]):
            if parent[y] == idx:
                has_child = True
                write_static(y, prefix + '\t')
        if not has_child:
            file_string += prefix + '\t' + 'End Site\n'
            file_string += prefix + '\t' + '{\n'
            file_string += prefix + '\t\t' + 'OFFSET 0 0 0\n'
            file_string += prefix + '\t' + '}\n'

        file_string += prefix + '}\n'

    write_static(0, '')

    file_string += 'MOTION\n' + 'Frames: {}\n'.format(frame) + 'Frame Time: %.8f\n' % frametime
    for i in range(frame):
        file_string += '%.6f %.6f %.6f ' % (position[i][0], position[i][1], position[i][2])
        for j in range(joint_num):
            idx = seq[j]
            file_string += '%.6f %.6f %.6f ' % (rotation[i][idx][0], rotation[i][idx][1], rotation[i][idx][2])
        file_string += '\n'

    file.write(file_string)
    return file_string


class WriterWrapper:
    def __init__(self, parents, offset=None):
        self.parents = parents
        self.offset = offset

    def write(self, filename, rot, pos, offset=None, names=None, repr='quat'):
        """
        Write animation to bvh file
        :param filename:
        :param rot: Quaternion as (w, x, y, z)
        :param pos:
        :param offset:
        :return:
        """
        if repr not in ['euler', 'quat', 'quaternion', 'repr6d']:
            raise Exception('Unknown rotation representation')
        if offset is None:
            offset = self.offset
        if not isinstance(offset, torch.Tensor):
            offset = torch.tensor(offset)
        n_bone = offset.shape[0]

        if repr == 'repr6d':
            rot = rot.reshape(rot.shape[0], -1, 6)
            rot = repr6d2quat(rot)
        if repr == 'repr6d' or repr == 'quat' or repr == 'quaternion':
            rot = rot.reshape(rot.shape[0], -1, 4)
            rot /= rot.norm(dim=-1, keepdim=True) ** 0.5
            euler = quat2euler(rot, order='xyz')
            rot = euler

        if names is None:
            names = ['%02d' % i for i in range(n_bone)]
        write_bvh(self.parents, offset, rot, pos, names, 1, 'xyz', filename)
