using System.Collections.Generic;
using UnityEngine;

namespace pdinklag.CPW.Common
{
    /// <summary>
    /// Provides utilities for automatic wrestler body generation.
    /// </summary>
    public class BodyDescriptionGenerator
    {
        private static readonly float SCAN_OFFSET = 2.0f;
        private static readonly float SCAN_DISTANCE = 3.0f;

        /// <summary>
        /// Provides hints to the generator.
        /// </summary>
        public struct GeneratorHints
        {
            public static GeneratorHints Default
            {
                get
                {
                    GeneratorHints hints;
                    hints.EyesXFromCenter = 0.05f; // between 0 and 1
                    hints.EyesYFromBottom = 0.925f;
                    return hints;
                }
            }

            public float EyesXFromCenter, EyesYFromBottom;
        }

        private readonly BodyDescription _desc;
        public MeshRaycast Mesh { get; private set; }
        public GeneratorHints Hints = GeneratorHints.Default;

        public BodyDescriptionGenerator(BodyDescription desc)
        {
            _desc = desc;

            var oldPos = desc.transform.position;
            var oldRot = desc.transform.rotation;
            desc.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
            Mesh = new MeshRaycast(desc.transform);
            desc.transform.SetPositionAndRotation(oldPos, oldRot);
        }

        public void Destroy()
        {
            Mesh.Destroy();
        }

        private Axis DetectAxis(Transform xform, Vector3 target)
        {
            // try each axis and find that with the lowest difference from the target
            var best = (Axis)0;
            var bestAngle = Mathf.Abs(Vector3.Angle(best.For(xform), target));
            for (var i = 1; i < 6; i++)
            {
                var axis = (Axis)i;
                var angle = Mathf.Abs(Vector3.Angle(axis.For(xform), target));
                if (angle < bestAngle)
                {
                    bestAngle = angle;
                    best = axis;
                }
            }
            return best;
        }

        private BoneOrientation DetectBoneOrientationTPose(Transform xform, Vector3 forward, Vector3 up)
        {
            return new BoneOrientation(
                DetectAxis(xform, forward),
                DetectAxis(xform, up));
        }

        public void DetectBoneOrientationsTPose()
        {
            // initialize
            var anim = _desc.GetComponent<Animator>();
            var fwd = _desc.transform.forward;
            var up = _desc.transform.up;

            _desc.HeadOrientation = DetectBoneOrientationTPose(HumanBodyBones.Head.For(anim), fwd, up);
            _desc.SpineOrientation = DetectBoneOrientationTPose(HumanBodyBones.Spine.For(anim), fwd, up);
            _desc.LeftUpperArmOrientation = DetectBoneOrientationTPose(HumanBodyBones.LeftUpperArm.For(anim), fwd, up);
            _desc.LeftLowerArmOrientation = DetectBoneOrientationTPose(HumanBodyBones.LeftLowerArm.For(anim), fwd, up);
            _desc.LeftHandOrientation = DetectBoneOrientationTPose(HumanBodyBones.LeftHand.For(anim), fwd, up);
            _desc.RightUpperArmOrientation = DetectBoneOrientationTPose(HumanBodyBones.RightUpperArm.For(anim), fwd, up);
            _desc.RightLowerArmOrientation = DetectBoneOrientationTPose(HumanBodyBones.RightLowerArm.For(anim), fwd, up);
            _desc.RightHandOrientation = DetectBoneOrientationTPose(HumanBodyBones.RightHand.For(anim), fwd, up);
            _desc.LeftUpperLegOrientation = DetectBoneOrientationTPose(HumanBodyBones.LeftUpperLeg.For(anim), fwd, up);
            _desc.LeftLowerLegOrientation = DetectBoneOrientationTPose(HumanBodyBones.LeftLowerLeg.For(anim), fwd, up);
            _desc.LeftFootOrientation = DetectBoneOrientationTPose(HumanBodyBones.LeftFoot.For(anim), fwd, up);
            _desc.RightUpperLegOrientation = DetectBoneOrientationTPose(HumanBodyBones.RightUpperLeg.For(anim), fwd, up);
            _desc.RightLowerLegOrientation = DetectBoneOrientationTPose(HumanBodyBones.RightLowerLeg.For(anim), fwd, up);
            _desc.RightFootOrientation = DetectBoneOrientationTPose(HumanBodyBones.RightFoot.For(anim), fwd, up);
        }

        private Vector3 MeshCastTo(Vector3 end, Vector3 dir)
        {
            dir = dir.normalized;
            if (Mesh.Raycast(end - SCAN_OFFSET * dir, dir, SCAN_DISTANCE, out var hit))
            {
                return hit.point;
            }
            else
            {
                Debug.LogWarningFormat("mesh cast didn't hit anything");
                return Vector3.zero;
            }
        }

        private void MeshScan(Vector3 startPoint, Vector3 endPoint, Vector3 dir, int numPoints, IList<RaycastHit> outHits)
        {
            dir = dir.normalized;
            Mesh.Scan(startPoint - SCAN_OFFSET * dir, endPoint - SCAN_OFFSET * dir, numPoints, dir, SCAN_DISTANCE, outHits);
            if (outHits.Count == 0)
            {
                Debug.LogWarningFormat("mesh scan didn't find anything");
            }
        }

        /// <summary>
        /// Generates the body description.
        /// </summary>
        /// <returns>A list of newly created objects.</returns>
        public IEnumerable<BodyPartComponent> GenerateFromTPose()
        {
            var created = new HashSet<BodyPartComponent>();

            // reset position and rotation
            var oldPos = _desc.transform.position;
            var oldRot = _desc.transform.rotation;
            _desc.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);

            // detect bone orientations
            DetectBoneOrientationsTPose();

            // get animator and bones
            var anim = _desc.GetComponent<Animator>();
            var bones = new AnimatorBones(anim);

            // create ragdoll objects
            GenerateIfNotExists(ref _desc.Head, "Head (Ragdoll Part)", bones[HumanBodyBones.Head], Color.white, created);
            GenerateIfNotExists(ref _desc.UpperTorso, "UpperTorso (Ragdoll Part)", bones[HumanBodyBones.Spine], Color.white, created);
            GenerateIfNotExists(ref _desc.LowerTorso, "LowerTorso (Ragdoll Part)", bones[HumanBodyBones.Hips], Color.white, created);
            GenerateIfNotExists(ref _desc.LeftUpperArm, "LeftUpperArm (Ragdoll Part)", bones[HumanBodyBones.LeftUpperArm], Color.white, created);
            GenerateIfNotExists(ref _desc.LeftLowerArm, "LeftLowerArm (Ragdoll Part)", bones[HumanBodyBones.LeftLowerArm], Color.white, created);
            GenerateIfNotExists(ref _desc.LeftHand, "LeftHand (Ragdoll Part)", bones[HumanBodyBones.LeftHand], Color.white, created);
            GenerateIfNotExists(ref _desc.LeftUpperLeg, "LeftUpperLeg (Ragdoll Part)", bones[HumanBodyBones.LeftUpperLeg], Color.white, created);
            GenerateIfNotExists(ref _desc.LeftLowerLeg, "LeftLowerLeg (Ragdoll Part)", bones[HumanBodyBones.LeftLowerLeg], Color.white, created);
            GenerateIfNotExists(ref _desc.LeftFoot, "LeftFoot (Ragdoll Part)", bones[HumanBodyBones.LeftFoot], Color.white, created);
            GenerateIfNotExists(ref _desc.RightUpperArm, "RightUpperArm (Ragdoll Part)", bones[HumanBodyBones.RightUpperArm], Color.white, created);
            GenerateIfNotExists(ref _desc.RightLowerArm, "RightLowerArm (Ragdoll Part)", bones[HumanBodyBones.RightLowerArm], Color.white, created);
            GenerateIfNotExists(ref _desc.RightHand, "RightHand (Ragdoll Part)", bones[HumanBodyBones.RightHand], Color.white, created);
            GenerateIfNotExists(ref _desc.RightUpperLeg, "RightUpperLeg (Ragdoll Part)", bones[HumanBodyBones.RightUpperLeg], Color.white, created);
            GenerateIfNotExists(ref _desc.RightLowerLeg, "RightLowerLeg (Ragdoll Part)", bones[HumanBodyBones.RightLowerLeg], Color.white, created);
            GenerateIfNotExists(ref _desc.RightFoot, "RightFoot (Ragdoll Part)", bones[HumanBodyBones.RightFoot], Color.white, created);

            // create POIs
            if (!_desc.HasPoints)
            {
                _desc.Points = new BodyPartPoint[(int)BodyPOI.Last];
            }

            for (var i = 0; i < (int)BodyPOI.Last; i++)
            {
                var poi = (BodyPOI)i;
                GenerateIfNotExists(ref _desc.Points[i], poi.ToString() + " (POI)", poi.GetPreferredParentBone(anim), poi.GetPreferredColor(), created);
            }

            // create raycast target
            var meshBounds = Mesh.Bounds;

            var meshCenter = meshBounds.center;
            var meshExtX = meshBounds.extents.x * Vector3.right;
            var meshExtY = meshBounds.extents.y * Vector3.up;
            var meshExtZ = meshBounds.extents.z * Vector3.forward;
            var meshSizeX = meshBounds.size.x * Vector3.right;
            var meshSizeY = meshBounds.size.y * Vector3.up;
            var meshSizeZ = meshBounds.size.z * Vector3.forward;

            var hitBuffer = new List<RaycastHit>();

            // measure body, compute POI locations, define volumetrics
            {
                /*
                    HEAD & NECK
                */
                var headBone = bones[HumanBodyBones.Head].position;
                var neck = bones[HumanBodyBones.Neck] ? bones[HumanBodyBones.Neck].position : headBone;

                // scan for top of head
                MeshScan(headBone + meshExtZ, headBone - meshExtZ, Vector3.down, 64, hitBuffer);
                var headTop = neck;
                foreach (var hit in hitBuffer)
                {
                    if (hit.point.y > headTop.y) headTop = hit.point;
                }

                var headCenter = Vector3.Lerp(neck, headTop, 0.5f);
                var headLeft = MeshCastTo(headCenter, Vector3.right);
                var headRight = MeshCastTo(headCenter, Vector3.left);
                var headBack = MeshCastTo(headCenter, Vector3.forward);
                var headFront = MeshCastTo(headCenter, Vector3.back);
                headCenter = Vector3.Lerp(headFront, headBack, 0.5f);

                var headRadius = 0.5f * Mathf.Max(Vector3.Distance(headLeft, headRight), Vector3.Distance(headFront, headBack));
                var headHeight = Vector3.Distance(headTop, neck);
                var headRot = Quaternion.FromToRotation(Vector3.up, headTop - neck);

                MeshScan(headBone, headTop, Vector3.back, 64, hitBuffer);
                var noseTip = headFront;
                foreach (var hit in hitBuffer)
                {
                    if (hit.point.z > noseTip.z) noseTip = hit.point;
                }

                var forehead = MeshCastTo(Vector3.Lerp(noseTip, headTop, MathExt.GoldenRatioInv), Vector3.back);
                var leftParietal = MeshCastTo(headCenter.WithY(forehead.y), Vector3.right);
                var partietalBack = MeshCastTo(headCenter.WithY(forehead.y), Vector3.forward);

                var leftEyeSocket = bones[HumanBodyBones.LeftEye] ? bones[HumanBodyBones.LeftEye].position : Vector3.Lerp(noseTip, forehead, 0.5f) + 0.33f * headRadius * Vector3.left;
                var leftEye = MeshCastTo(leftEyeSocket, Vector3.back);
                var leftEyebrow = MeshCastTo(leftEye.WithY(Mathf.Lerp(forehead.y, leftEye.y, MathExt.GoldenRatioInv)), Vector3.back);

                var occipitalBack = MeshCastTo(headCenter.WithY(leftEye.y), Vector3.forward);
                var noseBridge = MeshCastTo(noseTip.WithY(leftEye.y), Vector3.back);

                var leftTemple = MeshCastTo(Vector3.Lerp(occipitalBack, noseBridge, MathExt.GoldenRatioInv), Vector3.right);
                var eyeWidth = leftEye.x;
                var leftCheekbone = MeshCastTo(noseTip.WithX(leftEye.x + 0.5f * eyeWidth), Vector3.back);

                var leftEarCenter = MeshCastTo(
                    Vector3.Lerp(noseBridge, occipitalBack, MathExt.GoldenRatioInv).WithY(Mathf.Lerp(noseTip.y, noseBridge.y, 0.5f)),
                    Vector3.right);

                var earHeight = Mathf.Abs(leftEyebrow.y - noseTip.y);

                var leftEarBehind = MeshCastTo(leftEarCenter + 0.5f * earHeight * Vector3.up, Vector3.right);
                var leftEarBeyondTop = MeshCastTo(leftEarCenter + 0.75f * earHeight * Vector3.up, Vector3.right);
                if (Mesh.Raycast(
                        Vector3.Lerp(leftEarBeyondTop, leftEarCenter.WithY(leftEarBeyondTop.y), 0.5f),
                        Vector3.down, SCAN_DISTANCE, out var leftEarHit)
                    && leftEarHit.point.y > leftEarCenter.y)
                {
                    leftEarBehind = leftEarHit.point;
                }

                var earDepth = Mathf.Abs(leftCheekbone.z - noseBridge.z);
                var leftEarlobe = MeshCastTo(leftEarCenter.WithY(noseTip.y) + 0.375f * earDepth * Vector3.forward, Vector3.right);

                var chin = MeshCastTo(noseTip + Mathf.Abs(noseTip.y - leftEyebrow.y) * Vector3.down, Vector3.back);

                var lowerLip = MeshCastTo(chin.WithY(Mathf.Lerp(noseTip.y, chin.y, MathExt.GoldenRatioInv)), Vector3.back);
                var upperLip = MeshCastTo(lowerLip.WithY(Mathf.Lerp(noseTip.y, lowerLip.y, 0.5f)), Vector3.back);

                var mouthCenter = Vector3.Lerp(lowerLip, upperLip, 0.5f);
                var leftMouthCorner = MeshCastTo(mouthCenter.WithX(leftEye.x), Vector3.back);
                var leftJawAngle = MeshCastTo(leftTemple.WithY(mouthCenter.y), Vector3.right);

                var throat = MeshCastTo(chin + Mathf.Abs(chin.y - upperLip.y) * Vector3.down, Vector3.back);
                var neckBack = MeshCastTo(chin, Vector3.forward);
                var leftNeckSide = MeshCastTo(Vector3.Lerp(throat, neckBack, MathExt.GoldenRatioInv).WithZ(neck.z), Vector3.right + 0.5f * Vector3.down);

                // write
                {
                    _desc.Head.transform.position = new Vector3(0, Mathf.Lerp(headTop.y, neck.y, 0.5f), Mathf.Lerp(forehead.z, occipitalBack.z, 0.5f));
                    _desc.Head.Size = new Vector3(2.0f * Mathf.Abs(headCenter.x - leftEarCenter.x), headHeight, Mathf.Abs(forehead.z - occipitalBack.z));
                    _desc.HeadTop.transform.position = headTop;
                    _desc.Forehead.transform.position = forehead;
                    _desc.LeftParietal.transform.position = leftParietal;
                    _desc.ParietalBack.transform.position = partietalBack;
                    _desc.OccipitalBack.transform.position = occipitalBack;
                    _desc.NoseTip.transform.position = noseTip;
                    _desc.NoseBridge.transform.position = noseBridge;
                    _desc.LeftEarCenter.transform.position = leftEarCenter;
                    _desc.LeftEarBehind.transform.position = leftEarBehind;
                    _desc.LeftEarlobe.transform.position = leftEarlobe;
                    _desc.LeftTemple.transform.position = leftTemple;
                    _desc.LeftEye.transform.position = leftEye;
                    _desc.LeftEyebrow.transform.position = leftEyebrow;
                    _desc.LeftCheekbone.transform.position = leftCheekbone;
                    _desc.UpperLip.transform.position = upperLip;
                    _desc.LowerLip.transform.position = lowerLip;
                    _desc.LeftMouthCorner.transform.position = leftMouthCorner;
                    _desc.LeftJawAngle.transform.position = leftJawAngle;
                    _desc.Chin.transform.position = chin;
                    _desc.Throat.transform.position = throat;
                    _desc.LeftNeckSide.transform.position = leftNeckSide;
                    _desc.NeckBack.transform.position = neckBack;
                }

                /*
                    TORSO
                */
                var lowerSpine = bones[HumanBodyBones.Spine].position;

                var kneeMiddle = Vector3.Lerp(bones[HumanBodyBones.LeftLowerLeg].position, bones[HumanBodyBones.RightLowerLeg].position, 0.5f);
                var perineum = Mesh.Raycast(kneeMiddle, Vector3.up, SCAN_DISTANCE);

                var hipsSizeY = Mathf.Abs(lowerSpine.y - perineum.y);
                var lowerTorso = perineum + 0.5f * hipsSizeY * Vector3.up;

                var lowerTorsoLeft = MeshCastTo(lowerTorso, Vector3.right);
                var lowerTorsoRight = MeshCastTo(lowerTorso, Vector3.left);
                var hipsSizeX = Mathf.Abs(lowerTorsoRight.x - lowerTorsoLeft.x);

                var lowerTorsoFront = MeshCastTo(lowerTorso, Vector3.back);
                var lowerTorsoBack = MeshCastTo(lowerTorso, Vector3.forward);
                var hipsSizeZ = Mathf.Abs(lowerTorsoFront.z - lowerTorsoBack.z);

                lowerTorso = Vector3.Lerp(lowerTorsoFront, lowerTorsoBack, 0.5f);
                var groin = MeshCastTo(Vector3.Lerp(lowerTorso, perineum, 0.5f), Vector3.back);
                var buttocks = lowerTorsoBack;
                var leftPelvisLateral = MeshCastTo(lowerTorso, Vector3.right);

                var hipsTopCenter = perineum + hipsSizeY * Vector3.up;
                var belly = MeshCastTo(hipsTopCenter, Vector3.back);
                var lumbar = MeshCastTo(hipsTopCenter, Vector3.forward);
                var leftWaist = MeshCastTo(hipsTopCenter, Vector3.right);

                var spineSizeY = Mathf.Abs(neck.y - lowerSpine.y);
                var upperTorso = lowerSpine + 0.5f * spineSizeY * Vector3.up;

                var leftUpperArm = bones[HumanBodyBones.LeftUpperArm].position;
                var rightUpperArm = bones[HumanBodyBones.RightUpperArm].position;
                var spineSizeX = Mathf.Abs(leftUpperArm.x - rightUpperArm.x);

                var upperTorsoFront = MeshCastTo(upperTorso, Vector3.back);
                var upperTorsoBack = MeshCastTo(upperTorso, Vector3.forward);
                var spineSizeZ = Mathf.Abs(upperTorsoFront.z - upperTorsoBack.z);

                upperTorso = Vector3.Lerp(upperTorsoFront, upperTorsoBack, 0.5f);

                var shoulderCenter = Vector3.Lerp(leftUpperArm, rightUpperArm, 0.5f);
                var chestCenter = Vector3.Lerp(shoulderCenter, upperTorso, 0.5f);

                var sternum = MeshCastTo(chestCenter, Vector3.back);
                var upperSpine = MeshCastTo(chestCenter, Vector3.forward);

                var abdominal = Vector3.Lerp(lowerTorso, upperTorso, MathExt.GoldenRatioInv);
                var stomach = MeshCastTo(abdominal, Vector3.back);
                var thoracic = MeshCastTo(abdominal, Vector3.forward);
                var leftAbdomen = MeshCastTo(abdominal, Vector3.right);

                var lowerSternumCenter = Vector3.Lerp(abdominal, chestCenter, MathExt.GoldenRatioInv);
                var leftLateral = MeshCastTo(lowerSternumCenter, Vector3.right);
                MeshScan(lowerSternumCenter + 0.5f * spineSizeX * Vector3.left, lowerSternumCenter, Vector3.back, 64, hitBuffer);
                var leftNipple = hitBuffer[hitBuffer.Count / 4].point; // lower quartile

                var leftShoulderCenter = Vector3.Lerp(shoulderCenter, leftUpperArm, MathExt.GoldenRatioInv);
                var leftShoulderFront = MeshCastTo(leftShoulderCenter, Vector3.back);
                var leftShoulderBack = MeshCastTo(leftShoulderCenter, Vector3.forward);
                var leftShoulderLateral = MeshCastTo(leftUpperArm, Vector3.down);
                var leftTrapezius = MeshCastTo(leftShoulderCenter.WithY(headCenter.y), Vector3.down + 0.25f * Vector3.right);

                var leftPectorial = MeshCastTo(sternum.WithX(leftShoulderFront.x), Vector3.back);

                // write
                {
                    _desc.LowerTorso.transform.position = lowerTorso;
                    _desc.LowerTorso.transform.rotation = Quaternion.FromToRotation(Vector3.up, lowerSpine - perineum);
                    _desc.LowerTorso.Size = new Vector3(hipsSizeX, hipsSizeY, hipsSizeZ);
                    _desc.UpperTorso.transform.position = upperTorso;
                    _desc.UpperTorso.transform.rotation = Quaternion.FromToRotation(Vector3.up, neck - lowerSpine);
                    _desc.UpperTorso.Size = new Vector3(spineSizeX, spineSizeY, spineSizeZ);
                    _desc.Perineum.transform.position = perineum;
                    _desc.Groin.transform.position = groin;
                    _desc.Buttocks.transform.position = buttocks;
                    _desc.LeftPelvisLateral.transform.position = leftPelvisLateral;
                    _desc.Belly.transform.position = belly;
                    _desc.Lumbar.transform.position = lumbar;
                    _desc.LeftWaist.transform.position = leftWaist;
                    _desc.Sternum.transform.position = sternum;
                    _desc.UpperSpine.transform.position = upperSpine;
                    _desc.Stomach.transform.position = stomach;
                    _desc.Thoracic.transform.position = thoracic;
                    _desc.LeftAbdomen.transform.position = leftAbdomen;
                    _desc.LeftLateral.transform.position = leftLateral;
                    _desc.LeftShoulderFront.transform.position = leftShoulderFront;
                    _desc.LeftShoulderBack.transform.position = leftShoulderBack;
                    _desc.LeftShoulderLateral.transform.position = leftShoulderLateral;
                    _desc.LeftTrapezius.transform.position = leftTrapezius;
                    _desc.LeftPectorial.transform.position = leftPectorial;
                    _desc.LeftNipple.transform.position = leftNipple;
                }

                /*
                    ARMS & HANDS
                */
                var leftLowerArm = bones[HumanBodyBones.LeftLowerArm].position;

                var leftUpperArmLength = Vector3.Distance(leftUpperArm, leftLowerArm);
                var leftUpperArmCenter = Vector3.Lerp(leftUpperArm, leftLowerArm, 0.5f);

                var leftUpperArmBack = MeshCastTo(leftUpperArmCenter, Vector3.forward);
                var leftUpperArmFront = MeshCastTo(leftUpperArmCenter, Vector3.back);
                var leftUpperArmLateral = MeshCastTo(leftUpperArmCenter, Vector3.down);
                var leftUpperArmMedial = MeshCastTo(leftUpperArmCenter, Vector3.up);

                var leftUpperArmRadius = 0.5f * Mathf.Max(
                    Vector3.Distance(leftUpperArmBack, leftUpperArmFront), Vector3.Distance(leftUpperArmMedial, leftUpperArmLateral));

                var leftArmpitStart = leftUpperArmCenter.WithY(upperTorso.y);
                var leftArmpit = Mesh.Raycast(leftArmpitStart, leftUpperArm - leftArmpitStart, SCAN_DISTANCE);

                var leftElbowBackDir = Vector3.Cross(leftLowerArm - leftUpperArm, Vector3.up);
                var leftElbow = MeshCastTo(leftLowerArm, -leftElbowBackDir);
                var leftInnerElbow = MeshCastTo(leftLowerArm, leftElbowBackDir);
                var leftElbowLateral = MeshCastTo(leftLowerArm, Vector3.down);
                var leftElbowMedial = MeshCastTo(leftLowerArm, Vector3.up);

                var leftHandBone = bones[HumanBodyBones.LeftHand].position;
                var leftLowerArmLength = Vector3.Distance(leftLowerArm, leftHandBone);
                var leftLowerArmCenter = Vector3.Lerp(leftLowerArm, leftHandBone, 0.5f);

                var leftLowerArmBack = MeshCastTo(leftLowerArmCenter, Vector3.forward);
                var leftLowerArmFront = MeshCastTo(leftLowerArmCenter, Vector3.back);
                var leftLowerArmLateral = MeshCastTo(leftLowerArmCenter, Vector3.down);
                var leftLowerArmMedial = MeshCastTo(leftLowerArmCenter, Vector3.up);

                var leftLowerArmRadius = 0.5f * Mathf.Max(
                    Vector3.Distance(leftLowerArmBack, leftLowerArmFront), Vector3.Distance(leftLowerArmLateral, leftLowerArmMedial));

                var leftWristCenter = Vector3.Lerp(leftLowerArmCenter, leftHandBone, MathExt.GoldenRatioInv);
                var leftWristBackDir = Vector3.Cross(leftWristCenter - leftLowerArmCenter, Vector3.up);
                var leftWristFront = MeshCastTo(leftWristCenter, leftWristBackDir);
                var leftWristBack = MeshCastTo(leftWristCenter, -leftWristBackDir);
                var leftWristLateral = MeshCastTo(leftWristCenter, Vector3.down);
                var leftWristMedial = MeshCastTo(leftWristCenter, Vector3.up);

                var lastLeftThumbBone = anim.GetFirstNonNullBone(new(
                        HumanBodyBones.LeftThumbDistal, HumanBodyBones.LeftThumbIntermediate, HumanBodyBones.LeftThumbProximal, HumanBodyBones.LeftHand));
                var leftThumbAxis = (lastLeftThumbBone.position - lastLeftThumbBone.parent.position);
                var leftThumbTip = MeshCastTo(lastLeftThumbBone.position, -leftThumbAxis);

                var lastLeftIndexBone = anim.GetFirstNonNullBone(new(
                        HumanBodyBones.LeftIndexDistal, HumanBodyBones.LeftIndexIntermediate, HumanBodyBones.LeftIndexProximal, HumanBodyBones.LeftHand));
                var leftIndexAxis = DetectAxis(lastLeftIndexBone, Vector3.left).For(lastLeftIndexBone); // negative X direction
                var leftIndexTip = MeshCastTo(lastLeftIndexBone.position, -leftIndexAxis);

                var leftHandLength = Vector3.Distance(leftIndexTip, leftHandBone);
                var leftHandAxis = DetectAxis(bones[HumanBodyBones.LeftHand], Vector3.left).For(bones[HumanBodyBones.LeftHand]); // negative X direction
                var leftFingerLength = bones[HumanBodyBones.LeftIndexProximal]
                    ? Vector3.Distance(bones[HumanBodyBones.LeftIndexProximal].position, leftIndexTip)
                    : MathExt.GoldenRatioInv * leftHandLength;
                var leftHandCenter = leftHandBone + 0.5f * Mathf.Abs(leftHandLength - leftFingerLength) * leftHandAxis;

                var leftHandPalm = MeshCastTo(leftHandCenter, Vector3.up);
                var leftHandBack = MeshCastTo(leftHandCenter, Vector3.down);
                var leftHandSide = MeshCastTo(leftHandCenter, Vector3.forward);

                var leftKnuckle = bones[HumanBodyBones.LeftMiddleProximal]
                    ? MeshCastTo(bones[HumanBodyBones.LeftMiddleProximal].position, Vector3.down)
                    : MeshCastTo(Vector3.Lerp(leftHandBack, leftIndexTip, 0.5f), Vector3.down);

                // write
                {
                    _desc.LeftUpperArm.transform.position =
                        new InlineArray<Vector3>(leftUpperArmBack, leftUpperArmFront, leftUpperArmLateral, leftUpperArmMedial).Center();
                    _desc.LeftUpperArm.transform.rotation = Quaternion.FromToRotation(Vector3.up, leftLowerArm - leftUpperArm);
                    _desc.LeftUpperArm.Height = leftUpperArmLength;
                    _desc.LeftUpperArm.Radius = leftUpperArmRadius;
                    _desc.LeftUpperArmBack.transform.position = leftUpperArmBack;
                    _desc.LeftUpperArmFront.transform.position = leftUpperArmFront;
                    _desc.LeftUpperArmMedial.transform.position = leftUpperArmMedial;
                    _desc.LeftUpperArmLateral.transform.position = leftUpperArmLateral;
                    _desc.LeftArmpit.transform.position = leftArmpit;
                    _desc.LeftElbow.transform.position = leftElbow;
                    _desc.LeftInnerElbow.transform.position = leftInnerElbow;
                    _desc.LeftElbowMedial.transform.position = leftElbowMedial;
                    _desc.LeftElbowLateral.transform.position = leftElbowLateral;
                    _desc.LeftLowerArm.transform.position =
                        new InlineArray<Vector3>(leftLowerArmBack, leftLowerArmFront, leftLowerArmLateral, leftLowerArmMedial).Center();
                    _desc.LeftLowerArm.transform.rotation = Quaternion.FromToRotation(Vector3.up, leftHandBone - leftLowerArm);
                    _desc.LeftLowerArm.Height = leftLowerArmLength;
                    _desc.LeftLowerArm.Radius = leftLowerArmRadius;
                    _desc.LeftLowerArmBack.transform.position = leftLowerArmBack;
                    _desc.LeftLowerArmFront.transform.position = leftLowerArmFront;
                    _desc.LeftLowerArmMedial.transform.position = leftLowerArmMedial;
                    _desc.LeftLowerArmLateral.transform.position = leftLowerArmLateral;
                    _desc.LeftWristBack.transform.position = leftWristBack;
                    _desc.LeftWristFront.transform.position = leftWristFront;
                    _desc.LeftWristMedial.transform.position = leftWristMedial;
                    _desc.LeftWristLateral.transform.position = leftWristLateral;
                    _desc.LeftHand.transform.position = leftHandCenter;
                    _desc.LeftHand.transform.rotation = Quaternion.FromToRotation(Vector3.left, leftHandAxis);
                    _desc.LeftHand.Size = new(
                        2.0f * Vector3.Distance(leftHandCenter, leftHandBone),
                        Vector3.Distance(leftHandPalm, leftHandBack),
                        2.0f * Vector3.Distance(leftHandSide, leftHandCenter));
                    _desc.LeftIndexTip.transform.position = leftIndexTip;
                    _desc.LeftThumbTip.transform.position = leftThumbTip;
                    _desc.LeftHandPalm.transform.position = leftHandPalm;
                    _desc.LeftHandBack.transform.position = leftHandBack;
                    _desc.LeftHandSide.transform.position = leftHandSide;
                    _desc.LeftKnuckle.transform.position = leftKnuckle;
                }

                /*
                    LEGS & FEET
                */
                var leftUpperLeg = bones[HumanBodyBones.LeftUpperLeg].position;
                var leftLowerLeg = bones[HumanBodyBones.LeftLowerLeg].position;

                var leftUpperLegLength = Vector3.Distance(leftUpperLeg, leftLowerLeg);
                var leftUpperLegCenter = Vector3.Lerp(leftUpperLeg, leftLowerLeg, 0.5f);

                var leftUpperLegBack = MeshCastTo(leftUpperLegCenter, Vector3.forward);
                var leftUpperLegFront = MeshCastTo(leftUpperLegCenter, Vector3.back);
                var leftUpperLegLateral = MeshCastTo(leftUpperLegCenter, Vector3.right);
                var leftUpperLegMedial = Mesh.Raycast(leftUpperLegCenter.WithX(0), Vector3.left, SCAN_DISTANCE);

                var leftUpperLegRadius = 0.5f * Mathf.Max(
                    Vector3.Distance(leftUpperLegBack, leftUpperLegFront), Vector3.Distance(leftUpperLegLateral, leftUpperLegMedial));
                leftUpperLegCenter = new InlineArray<Vector3>(leftUpperLegBack, leftUpperLegFront, leftUpperLegLateral, leftUpperLegMedial).Center();

                var leftFootBone = bones[HumanBodyBones.LeftFoot].position;

                var leftLowerLegLength = Vector3.Distance(leftLowerLeg, leftFootBone);
                var leftLowerLegCenter = Vector3.Lerp(leftLowerLeg, leftFootBone, 0.5f);

                var leftLowerLegBack = MeshCastTo(leftLowerLegCenter, Vector3.forward);
                var leftLowerLegFront = MeshCastTo(leftLowerLegCenter, Vector3.back);
                var leftLowerLegLateral = MeshCastTo(leftLowerLegCenter, Vector3.right);
                var leftLowerLegMedial = Mesh.Raycast(leftLowerLegCenter.WithX(0), Vector3.left, SCAN_DISTANCE);

                var leftLowerLegRadius = 0.5f * Mathf.Max(
                    Vector3.Distance(leftLowerLegBack, leftLowerLegFront), Vector3.Distance(leftLowerLegLateral, leftLowerLegMedial));
                leftLowerLegCenter = new InlineArray<Vector3>(leftLowerLegBack, leftLowerLegFront, leftLowerLegLateral, leftLowerLegMedial).Center();

                var leftLowerLegBackDir = Vector3.Cross(leftFootBone - leftLowerLeg, Vector3.left);
                var leftKnee = MeshCastTo(leftLowerLeg, leftLowerLegBackDir);
                var leftBackKnee = MeshCastTo(leftLowerLeg, -leftLowerLegBackDir);
                var leftKneeLateral = MeshCastTo(leftLowerLeg, Vector3.right);
                var leftKneeMedial = Mesh.Raycast(leftLowerLeg.WithX(0), Vector3.left, SCAN_DISTANCE);
                var leftCalf = MeshCastTo(leftLowerLegCenter, -leftLowerLegBackDir);
                var leftShin = MeshCastTo(leftLowerLegCenter, leftLowerLegBackDir);
                var leftCalfLateral = MeshCastTo(leftLowerLegCenter, Vector3.right);
                var leftCalfMedial = Mesh.Raycast(leftLowerLegCenter.WithX(0), Vector3.left, SCAN_DISTANCE);

                var leftAchilles = MeshCastTo(leftFootBone, Vector3.forward);
                var leftAnkleFront = MeshCastTo(leftFootBone, Vector3.back);
                var leftAnkleLateral = MeshCastTo(leftFootBone, Vector3.right);
                var leftAnkleMedial = Mesh.Raycast(leftFootBone.WithX(0), Vector3.left, SCAN_DISTANCE);

                var leftHeelBottom = MeshCastTo(leftFootBone, Vector3.up);
                var footHeight = Mathf.Abs(leftFootBone.y - leftHeelBottom.y);
                var leftHeelBack = MeshCastTo(Vector3.Lerp(leftFootBone, leftHeelBottom, MathExt.GoldenRatioInv), Vector3.forward);

                MeshScan(leftFootBone, leftHeelBottom + footHeight * Vector3.down, Vector3.back, 64, hitBuffer);
                var leftFootTip = leftFootBone;
                foreach (var hit in hitBuffer)
                {
                    if (hit.point.z > leftFootTip.z) leftFootTip = hit.point;
                }

                var leftFootBottom = MeshCastTo(Vector3.Lerp(leftHeelBack, leftFootTip, 0.5f), Vector3.up);

                MeshScan(leftFootBottom, leftFootBottom + footHeight * Vector3.up, Vector3.right, 64, hitBuffer);
                var leftFootLateral = leftFootBottom;
                foreach (var hit in hitBuffer)
                {
                    if (hit.point.x < leftFootLateral.x) leftFootLateral = hit.point;
                }
                var leftFootMedial = Mesh.Raycast(leftFootLateral.WithX(0), Vector3.left, SCAN_DISTANCE);
                leftFootBottom.x = Mathf.Lerp(leftFootMedial.x, leftFootLateral.x, 0.5f);
                var leftInstep = Mesh.Raycast(leftFootBottom.WithY(leftShin.y), Vector3.down, SCAN_DISTANCE);
                var leftFootBall = MeshCastTo(Vector3.Lerp(leftFootBottom, leftFootTip, MathExt.GoldenRatioInv), Vector3.up);
                leftFootBall.x = leftFootBottom.x;
                var leftToesTop = Mesh.Raycast(leftFootBall.WithY(leftShin.y), Vector3.down, SCAN_DISTANCE);
                var leftToesLateral = MeshCastTo(Vector3.Lerp(leftFootBall, leftToesTop, 0.5f), Vector3.right);
                var leftToesMedial = Mesh.Raycast(leftToesLateral.WithX(0), Vector3.left, SCAN_DISTANCE);

                var leftFootSize = new Vector3(
                    Mathf.Abs(leftFootLateral.x - leftFootMedial.x),
                    Mathf.Abs(leftFootBottom.y - leftInstep.y),
                    Mathf.Abs(leftHeelBack.z - leftFootTip.z));
                var leftFootCenter = leftFootBottom + 0.5f * leftFootSize.y * Vector3.up;

                // write
                {
                    _desc.LeftUpperLeg.transform.position = leftUpperLegCenter;
                    _desc.LeftUpperLeg.transform.rotation = Quaternion.FromToRotation(Vector3.down, leftLowerLeg - leftUpperLeg);
                    _desc.LeftUpperLeg.Height = leftUpperLegLength;
                    _desc.LeftUpperLeg.Radius = leftUpperLegRadius;

                    _desc.LeftThighFront.transform.position = leftUpperLegFront;
                    _desc.LeftThighBack.transform.position = leftUpperLegBack;
                    _desc.LeftThighMedial.transform.position = leftUpperLegMedial;
                    _desc.LeftThighLateral.transform.position = leftUpperLegLateral;

                    _desc.LeftLowerLeg.transform.position = leftLowerLegCenter;
                    _desc.LeftLowerLeg.transform.rotation = Quaternion.FromToRotation(Vector3.down, leftFootBone - leftLowerLeg);
                    _desc.LeftLowerLeg.Height = leftLowerLegLength;
                    _desc.LeftLowerLeg.Radius = leftLowerLegRadius;

                    _desc.LeftKnee.transform.position = leftKnee;
                    _desc.LeftBackKnee.transform.position = leftBackKnee;
                    _desc.LeftKneeLateral.transform.position = leftKneeLateral;
                    _desc.LeftKneeMedial.transform.position = leftKneeMedial;

                    _desc.LeftCalf.transform.position = leftCalf;
                    _desc.LeftShin.transform.position = leftShin;
                    _desc.LeftCalfLateral.transform.position = leftCalfLateral;
                    _desc.LeftCalfMedial.transform.position = leftCalfMedial;

                    _desc.LeftAchillesHeel.transform.position = leftAchilles;
                    _desc.LeftAnkleFront.transform.position = leftAnkleFront;
                    _desc.LeftAnkleLateral.transform.position = leftAnkleLateral;
                    _desc.LeftAnkleMedial.transform.position = leftAnkleMedial;

                    _desc.LeftFoot.transform.position = leftFootCenter;
                    _desc.LeftFoot.transform.rotation = Quaternion.identity;
                    _desc.LeftFoot.Size = leftFootSize;

                    _desc.LeftFootTip.transform.position = leftFootTip;
                    _desc.LeftHeelBack.transform.position = leftHeelBack;
                    _desc.LeftHeelBottom.transform.position = leftHeelBottom;
                    _desc.LeftFootBottom.transform.position = leftFootBottom;
                    _desc.LeftFootLateral.transform.position = leftFootLateral;
                    _desc.LeftFootMedial.transform.position = leftFootMedial;
                    _desc.LeftInstep.transform.position = leftInstep;
                    _desc.LeftFootBall.transform.position = leftFootBall;
                    _desc.LeftToesTop.transform.position = leftToesTop;
                    _desc.LeftToesLateral.transform.position = leftToesLateral;
                    _desc.LeftToesMedial.transform.position = leftToesMedial;
                }
            }
            MirrorLeftToRight(false);

            // reset
            _desc.transform.SetPositionAndRotation(oldPos, oldRot);
            return created;
        }

        private bool GenerateIfNotExists<T>(ref T bp, string name, Transform parent, Color color, ISet<BodyPartComponent> created) where T : BodyPartComponent
        {
            if (bp)
            {
                return false;
            }
            else
            {
                bp = GenerateBodyPart<T>(name, parent, color, created);
                return true;
            }
        }

        private T GenerateBodyPart<T>(string name, Transform parent, Color color, ISet<BodyPartComponent> created) where T : BodyPartComponent
        {
            var go = new GameObject(name);
            go.transform.SetParent(parent, false);
            go.transform.rotation = Quaternion.identity;

            var bp = go.AddComponent<T>();
            bp.GizmoColor = color;

            created.Add(bp);
            return bp;
        }

        private void CopyBodyPart(BodyPartComponent source, BodyPartComponent target)
        {
            if (source && target)
            {
                var sourceTransform = source.transform;
                target.transform.SetPositionAndRotation(sourceTransform.position, sourceTransform.rotation);
            }
        }

        private void CopyCylinder(BodyPartCylindrical source, BodyPartCylindrical target)
        {
            if (source && target)
            {
                CopyBodyPart(source, target);
                target.Height = source.Height;
                target.Radius = source.Radius;
            }
        }

        private void CopyBox(BodyPartBox source, BodyPartBox target)
        {
            if (source && target)
            {
                CopyBodyPart(source, target);
                target.Size = source.Size;
            }
        }

        public void Copy(BodyDescription source)
        {
            // put both at origin
            var sourcePos = source.transform.position;
            var sourceRot = source.transform.rotation;
            source.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);

            var targetPos = _desc.transform.position;
            var targetRot = _desc.transform.rotation;
            _desc.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);

            // body parts
            if (_desc.HasPoints)
            {
                for (var i = 0; i < (int)BodyPOI.Last; i++)
                {
                    CopyBodyPart(source.Points[i], _desc.Points[i]);
                }
            }

            CopyBox(source.Head, _desc.Head);
            CopyBox(source.UpperTorso, _desc.UpperTorso);
            CopyBox(source.LowerTorso, _desc.LowerTorso);
            CopyCylinder(source.LeftUpperArm, _desc.LeftUpperArm);
            CopyCylinder(source.RightUpperArm, _desc.RightUpperArm);
            CopyCylinder(source.LeftLowerArm, _desc.LeftLowerArm);
            CopyCylinder(source.RightLowerArm, _desc.RightLowerArm);
            CopyBox(source.LeftHand, _desc.LeftHand);
            CopyBox(source.RightHand, _desc.RightHand);
            CopyCylinder(source.LeftUpperLeg, _desc.LeftUpperLeg);
            CopyCylinder(source.RightUpperLeg, _desc.RightUpperLeg);
            CopyCylinder(source.LeftLowerLeg, _desc.LeftLowerLeg);
            CopyCylinder(source.RightLowerLeg, _desc.RightLowerLeg);
            CopyBox(source.LeftFoot, _desc.LeftFoot);
            CopyBox(source.RightFoot, _desc.RightFoot);

            // restore
            source.transform.SetPositionAndRotation(sourcePos, sourceRot);
            _desc.transform.SetPositionAndRotation(targetPos, targetRot);

            // orientations
            _desc.HeadOrientation = source.HeadOrientation;
            _desc.SpineOrientation = source.SpineOrientation;
            _desc.LeftUpperArmOrientation = source.LeftUpperArmOrientation;
            _desc.LeftLowerArmOrientation = source.LeftLowerArmOrientation;
            _desc.LeftHandOrientation = source.LeftHandOrientation;
            _desc.RightUpperArmOrientation = source.RightUpperArmOrientation;
            _desc.RightLowerArmOrientation = source.RightLowerArmOrientation;
            _desc.RightHandOrientation = source.RightHandOrientation;
            _desc.LeftUpperLegOrientation = source.LeftUpperLegOrientation;
            _desc.LeftLowerLegOrientation = source.LeftLowerLegOrientation;
            _desc.LeftFootOrientation = source.LeftFootOrientation;
            _desc.RightUpperLegOrientation = source.RightUpperLegOrientation;
            _desc.RightLowerLegOrientation = source.RightLowerLegOrientation;
            _desc.RightFootOrientation = source.RightFootOrientation;
        }

        private static void CondSwap<T>(T a, T b, bool swap, out T outA, out T outB) where T : class
        {
            outA = swap ? b : a;
            outB = swap ? a : b;
        }

        private void MirrorBodyPart(BodyPartComponent source, BodyPartComponent target, bool swap = false)
        {
            if (source && target)
            {
                CondSwap(source, target, swap, out source, out target);

                target.transform.position = Vector3.Reflect(source.transform.position, Vector3.left);

                var rot = source.transform.rotation;
                rot.x *= -1;
                rot.w *= -1;
                target.transform.rotation = rot;
            }
        }

        private void MirrorCylinder(BodyPartCylindrical source, BodyPartCylindrical target, bool swap = false)
        {
            if (source && target)
            {
                CondSwap(source, target, swap, out source, out target);
                MirrorBodyPart(source, target, false);
                target.Radius = source.Radius;
                target.Height = source.Height;
            }
        }

        private void MirrorSphere(BodyPartSphere source, BodyPartSphere target, bool swap = false)
        {
            if (source && target)
            {
                CondSwap(source, target, swap, out source, out target);
                MirrorBodyPart(source, target, false);
                target.Radius = source.Radius;
            }
        }

        private void MirrorBox(BodyPartBox source, BodyPartBox target, bool swap = false)
        {
            if (source && target)
            {
                CondSwap(source, target, swap, out source, out target);
                MirrorBodyPart(source, target, false);
                target.Size = source.Size;
            }
        }

        public void MirrorLeftToRight(bool resetPos, bool swap = false)
        {
            var oldPos = _desc.transform.position;
            var oldRot = _desc.transform.rotation;
            if (resetPos)
            {
                _desc.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
            }

            for (var i = 0; i < (int)BodyPOI.Last; i++)
            {
                var poi = (BodyPOI)i;
                if (poi.BelongsToLeftHalf())
                {
                    MirrorBodyPart(_desc.Points[i], _desc.Points[(int)poi.Mirror()], swap);
                }
            }

            MirrorCylinder(_desc.LeftUpperArm, _desc.RightUpperArm, swap);
            MirrorCylinder(_desc.LeftLowerArm, _desc.RightLowerArm, swap);
            MirrorBox(_desc.LeftHand, _desc.RightHand, swap);
            MirrorCylinder(_desc.LeftUpperLeg, _desc.RightUpperLeg, swap);
            MirrorCylinder(_desc.LeftLowerLeg, _desc.RightLowerLeg, swap);
            MirrorBox(_desc.LeftFoot, _desc.RightFoot, swap);

            if (resetPos)
            {
                _desc.transform.SetPositionAndRotation(oldPos, oldRot);
            }
        }
    }
}
