using Luxand;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace ActiveLiveness
{
    /// <summary>
    /// Class for storing information about the location and state of a detected face
    /// </summary>
    class FaceLocator
    {
        /// <summary>Stabilizer value for smoothing face size changes</summary>
        public float stabilizer = 0.0f;
        /// <summary>Width of the ellipse around the detected face</summary>
        public float Width = 0.0f;
        /// <summary>Height of the ellipse around the detected face</summary>
        public float Height = 0.0f;
        /// <summary>Face rotation angle in radians</summary>
        public float Angle = 0.0f;
        /// <summary>Center coordinates of the face</summary>
        public PointF Center = new PointF(0,0);

        /// <summary>Number of consecutive frames in which the face was not detected</summary>
        public int TimesNotAppear = 0;

        /// <summary>Flag indicating whether this face is undergoing liveness check</summary>
        public bool isChecking = false;
        /// <summary>Current liveness check stage number (from 0 to STAGES_NUMBER-1)</summary>
        public int StageNumber = 0;

        /// <summary>Current face state (bit mask of commands)</summary>
        public int state;
    }
    public partial class MainForm : Form
    {
        /// <summary>
        /// Enumeration of possible states of the liveness check program
        /// </summary>
        enum ProgramState
        {
            psNormal,    // Normal display mode
            psToCheck,   // Waiting for check to begin
            psChecking,  // Performing liveness check
            psChecked    // Check completed successfully
        }
        /// <summary>Current program state</summary>
        ProgramState programState = ProgramState.psNormal;

        /// <summary>Number of frames during which information about a missing face is retained</summary>
        const int FRAMES_TO_KEEP_MISSED_FACE = 5;

        /// <summary>Total number of liveness check stages</summary>
        const int STAGES_NUMBER = 6;

        /// <summary>Threshold values for head rotation angle horizontally (left/right) [minimum, maximum]</summary>
        int [] HOR_CONF_LEVEL = { 3, 18 };
        /// <summary>Threshold values for head tilt angle upward [minimum, maximum]</summary>
        int [] UP_CONF_LEVEL = { 2, 5 };
        /// <summary>Threshold values for head tilt angle downward [minimum, maximum]</summary>
        int [] DOWN_CONF_LEVEL = { -2, -5 };
        /// <summary>Threshold confidence values for smile detection [minimum, maximum]</summary>
        float [] SMILE_CONF_LEVEL = { 0.3f, 0.6f };

        /// <summary>Set of left eye feature point indices for determining its center</summary>
        public static readonly int [] LEFT_EYE_SET = { (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE, (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE_INNER_CORNER,
            (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE_OUTER_CORNER, (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE_LOWER_LINE1, (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE_LOWER_LINE2,
            (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE_LOWER_LINE3, (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE_UPPER_LINE1, (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE_UPPER_LINE2,
            (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE_UPPER_LINE3, (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE_RIGHT_IRIS_CORNER, (int)FSDK.FacialFeatures.FSDKP_LEFT_EYE_LEFT_IRIS_CORNER};

        /// <summary>Set of right eye feature point indices for determining its center</summary>
        public static readonly int[] RIGHT_EYE_SET = { (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE, (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE_INNER_CORNER,
            (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE_OUTER_CORNER, (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE_LOWER_LINE1, (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE_LOWER_LINE2,
            (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE_LOWER_LINE3, (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE_UPPER_LINE1, (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE_UPPER_LINE2,
            (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE_UPPER_LINE3, (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE_LEFT_IRIS_CORNER, (int)FSDK.FacialFeatures.FSDKP_RIGHT_EYE_RIGHT_IRIS_CORNER};

        /// <summary>
        /// Enumeration of commands (actions) for liveness check
        /// Powers of two are used to enable bitwise operations
        /// </summary>
        enum Command
        {
            TrunLeft = 1,      // Turn head left
            TrunRight = 2,     // Turn head right
            LookStraight = 4,  // Look straight
            LookUp = 8,        // Look up
            LookDown = 16,     // Look down
            Smile = 32         // Smile
        }

        /// <summary>Array of all available commands for liveness check</summary>
        Command[] Commands = { Command.TrunLeft, Command.TrunRight, Command.LookStraight, Command.LookUp, Command.LookDown, Command.Smile };
        /// <summary>Array of commands that the user must perform at each stage</summary>
        Command[] CommandsToDo = new Command[STAGES_NUMBER];

        /// <summary>Name of the camera being used</summary>
        String cameraName;
        /// <summary>Flag indicating the need to close the application</summary>
        bool needClose = false;

        /// <summary>Mouse coordinates in the pictureBox1 coordinate system</summary>
        Point mouse = new Point(0, 0);

        /// <summary>Mouse coordinates in the original image coordinate system</summary>
        Point mouseImg = new Point(0, 0);

        /// <summary>Current frame for display</summary>
        private Image currentFrame = null;

        /// <summary>
        /// WinAPI function to release HBITMAP handles returned by FSDKCam.GrabFrame
        /// </summary>
        [DllImport("gdi32.dll")]
        static extern bool DeleteObject(IntPtr hObject);

        /// <summary>
        /// Constructor of the main application form
        /// </summary>
        public MainForm()
        {
            InitializeComponent();
            // Subscribe to Paint event for proper rendering
            pictureBox1.Paint += pictureBox1_Paint;
            // Set minimum window size
            this.MinimumSize = new Size(640, 480);
        }

        /// <summary>
        /// Form load event handler
        /// Initializes FaceSDK and configures the camera
        /// </summary>
        private void MainForm_Load(object sender, EventArgs e)
        {
            if (FSDK.FSDKE_OK != FSDK.ActivateLibrary("INSERT THE LICENSE KEY HERE"))
            {
                MessageBox.Show("Please insert the license key in the FSDK.ActivateLibrary() function.", "Error activating FaceSDK", MessageBoxButtons.OK, MessageBoxIcon.Error);
                Application.Exit();
            }

            FSDK.InitializeLibrary();
            FSDK.InitializeCapturing();

            string[] cameraList = FSDK.GetCameraList();

            if (cameraList.Length == 0)
            {
                MessageBox.Show("Please attach a camera", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                Application.Exit();
            }
            cameraName = cameraList[0];

            Luxand.FSDK.VideoFormatInfo[] formatList = Luxand.Camera.GetVideoFormatList(cameraName);

            // Choose the best camera from the list
            int index = 0;
            int formatIndex = 0;
            int max_height = 0, max_width = 0;
            foreach (var format in formatList)
            {
                if (format.Width > max_width && format.Height > max_height)
                {
                    max_width = format.Width;
                    max_height = format.Height;
                    formatIndex = index;
                }
                index++;
            }

            Luxand.Camera.SetVideoFormat(cameraName, formatList[formatIndex]);
        }

        /// <summary>
        /// Form closing event handler
        /// Sets flag to terminate video processing
        /// </summary>
        private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            needClose = true;
        }

        /// <summary>
        /// Calculates the center (average point) of a set of facial feature points
        /// </summary>
        /// <param name="points">Array of all facial feature points</param>
        /// <param name="numbers">Indices of points for which to calculate the center</param>
        /// <param name="size">Number of points in the set</param>
        /// <returns>Coordinates of the central point</returns>
        private PointF Center(FSDK.TPoint[] points, int [] numbers, int size)
        {
            PointF C = new PointF(0, 0);
            for (int i = 0; i < size; ++i)
            {
                C.X += points[numbers[i]].x;
                C.Y += points[numbers[i]].y;
            }
            C.X /= size;
            C.Y /= size;
            return C;
        }

        /// <summary>
        /// Checks if a point is inside a rotated ellipse
        /// </summary>
        /// <param name="point">Point to check</param>
        /// <param name="RecCenter">Center of the ellipse</param>
        /// <param name="RecWidth">Width of the ellipse</param>
        /// <param name="RecHeight">Height of the ellipse</param>
        /// <param name="RecAngle">Rotation angle of the ellipse in radians</param>
        /// <returns>True if the point is inside the ellipse</returns>
        private bool IsInside(Point point, PointF RecCenter, float RecWidth, float RecHeight, float RecAngle)
        {
            float xx = (point.X - RecCenter.X) * (float)Math.Cos(RecAngle) + (point.Y - RecCenter.Y) * (float)Math.Sin(RecAngle);
            float yy = (point.X - RecCenter.X) * (float)Math.Sin(RecAngle) - (point.Y - RecCenter.Y) * (float)Math.Cos(RecAngle);
            return (Math.Pow(2 * xx / RecWidth, 2) + Math.Pow(2 * yy / RecHeight, 2) <= 1);
        }

        /// <summary>
        /// Returns a text description of the command for display to the user
        /// </summary>
        /// <param name="Stage">Check stage number</param>
        /// <param name="CommandToDo">Command to be performed</param>
        /// <returns>String with stage number and command description</returns>
        private string CommandName(int Stage, Command CommandToDo)
        {
            string caption = Stage.ToString();
            if (CommandToDo == Command.LookDown)
                return caption + " Look down";
            if (CommandToDo == Command.LookStraight)
                return caption + " Look straight";
            if (CommandToDo == Command.LookUp)
                return caption + " Look up";
            if (CommandToDo == Command.Smile)
                return caption + " Make a smile";
            if (CommandToDo == Command.TrunLeft)
                return caption + " Turn left";
            if (CommandToDo == Command.TrunRight)
                return caption + " Turn right";
            return caption + " Do nothing";
        }

        /// <summary>
        /// Checks if the user is performing the specified command
        /// Analyzes head position and facial expression
        /// </summary>
        /// <param name="CommandToDo">Command code to check</param>
        /// <param name="tracker">Face tracker handle</param>
        /// <param name="ID">Tracked face identifier</param>
        /// <param name="Face">FaceLocator object with face information</param>
        /// <returns>True if the command is performed correctly</returns>
        private bool CheckCommand(int CommandToDo, int tracker, long ID, FaceLocator Face)
        {
            string AttributeValues;
            FSDK.GetTrackerFacialAttribute(tracker, 0, ID, "Expression", out AttributeValues, 1024);
            float smile = 0.0f;
            FSDK.GetValueConfidence(AttributeValues, "Smile", out smile);

            FSDK.GetTrackerFacialAttribute(tracker, 0, ID, "Angles", out AttributeValues, 1024);
            float pan = 0.0f;
            float tilt = 0.0f;
            FSDK.GetValueConfidence(AttributeValues, "Pan", out pan);
            FSDK.GetValueConfidence(AttributeValues, "Tilt", out tilt);


            int CurrentState = 0;
            if (Math.Abs(pan) < HOR_CONF_LEVEL[0])
                CurrentState = (int)Command.LookStraight;
            else if (Math.Abs(pan) > HOR_CONF_LEVEL[1])
                CurrentState = pan > 0 ? (int)Command.TrunRight : (int)Command.TrunLeft;

            if (CurrentState != 0)
                Face.state = Face.state & ~((int)Command.TrunLeft | (int)Command.TrunRight) | CurrentState;
            CurrentState = 0;
            if (tilt > UP_CONF_LEVEL[1])
                CurrentState = (int)Command.LookUp;
            else if (tilt < DOWN_CONF_LEVEL[1])
                CurrentState = (int)Command.LookDown;
            else if ((tilt > DOWN_CONF_LEVEL[0]) && (tilt < UP_CONF_LEVEL[0]))
                CurrentState = (int)Command.LookStraight;

            if (CurrentState != 0)
                Face.state = Face.state & ~((int)Command.LookUp | (int)Command.LookDown) | CurrentState;

            if ((Face.state & ((int)Command.TrunLeft | (int)Command.TrunRight | (int)Command.LookUp | (int)Command.LookDown)) != 0)
                Face.state &= ~((int)Command.LookStraight);

            if (smile < SMILE_CONF_LEVEL[0])
                Face.state &= ~((int)Command.Smile);
            else if (smile > SMILE_CONF_LEVEL[1])
                Face.state |= (int)Command.Smile;

            if ((CommandToDo & Face.state) == 0)
                return false;

            if (CommandToDo == (int)Command.Smile)
                return true;

            return (((int)Command.Smile & Face.state) == 0);
        }

        /// <summary>
        /// Handler for the liveness check start button click
        /// Initializes the camera and face tracker, starts the main video processing loop
        /// </summary>
        private void button1_Click(object sender, EventArgs e)
        {
            this.button1.Enabled = false;

            // Initialize camera
            Luxand.Camera camera = null;
            try
            {
                camera = new Luxand.Camera(cameraName);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error opening camera");
                Application.Exit();
            }

            // Creating tracker
            Luxand.Tracker tracker = new Luxand.Tracker();
            if (tracker == null)
            {
                MessageBox.Show("Error creating tracker", "Error");
                Application.Exit();
            }

            // set realtime face detection parameters
            tracker.SetMultipleParameters("RecognizeFaces=true; DetectFacialFeatures=true; HandleArbitraryRotations=true; DetermineFaceRotationAngle=true; InternalResizeWidth=256; FaceDetectionThreshold=5; DetectGender = true; DetectAge = true; DetectExpression = true; DetectAngles = true", out var errorPosition);

            Dictionary<long, FaceLocator> Faces = new Dictionary<long, FaceLocator>();

            while (!needClose)
            {
                long[] keys = new long[Faces.Keys.Count];
                Faces.Keys.CopyTo(keys, 0);

                if (programState == ProgramState.psNormal)
                    for (int i = 0; i < keys.Length; ++i)
                        Faces[keys[i]].isChecking = false;

                for (int i = 0; i < keys.Length; ++i)
                    if (Faces[keys[i]].TimesNotAppear > FRAMES_TO_KEEP_MISSED_FACE)
                    {
                        if (Faces[keys[i]].isChecking)
                            programState = ProgramState.psNormal;
                        Faces.Remove(keys[i]);
                    }
                    else
                        Faces[keys[i]].TimesNotAppear++;

                CImage frame = camera.GrabFrame();
                if (frame == null)
                {
                    Application.DoEvents();
                    continue;
                }
                Image frameImage = frame.ToCLRImage();

                // Process the frame with the tracker
                tracker.FeedFrame(frame, out long[] faceIds);

                // Make UI controls accessible (to find if the user clicked on a face)
                Application.DoEvents();

                // Update mouse coordinates in image coordinate system
                UpdateMouseImageCoords();

                if (faceIds != null)
                {
                    Graphics gr = Graphics.FromImage(frameImage);

                    foreach (var faceId in faceIds)
                    {
                        if (!Faces.ContainsKey(faceId))
                        {
                            Faces.Add(faceId, new FaceLocator());
                        }

                        FaceLocator CurrentFace = Faces[faceId];
                        CurrentFace.TimesNotAppear = 0;
                        
                        FSDK.TPoint[] facialFeatures = tracker.GetFacialFeatures(0, faceId);

                        PointF LeftEye = Center(facialFeatures, LEFT_EYE_SET, LEFT_EYE_SET.Length);
                        PointF RightEye = Center(facialFeatures, RIGHT_EYE_SET, RIGHT_EYE_SET.Length);

                        CurrentFace.Width = 2.6f * (float)Math.Sqrt(Math.Pow(LeftEye.X - RightEye.X, 2) + Math.Pow(LeftEye.Y - RightEye.Y, 2));

                        if (CurrentFace.stabilizer == 0)
                        {
                            CurrentFace.stabilizer = CurrentFace.Width;
                        }
                        else
                        {
                            CurrentFace.Width = (CurrentFace.stabilizer = 0.35f * CurrentFace.Width + 0.65f * CurrentFace.stabilizer);
                        }

                        CurrentFace.Height = 1.4f * CurrentFace.Width;
                        CurrentFace.Angle = (float)Math.Atan2(RightEye.Y - LeftEye.Y, RightEye.X - LeftEye.X);
                        CurrentFace.Center = new PointF((LeftEye.X + RightEye.X) / 2 - 0.05f * CurrentFace.Width * (float)Math.Sin(CurrentFace.Angle),
                            (LeftEye.Y + RightEye.Y) / 2 + 0.05f * CurrentFace.Width * (float)Math.Cos(CurrentFace.Angle));

                        gr.TranslateTransform(CurrentFace.Center.X, CurrentFace.Center.Y);
                        gr.RotateTransform(CurrentFace.Angle * 180 / (float)Math.PI);

                        Font TextFont = new Font(new FontFamily("Tahoma"), 14.0f);
                        Brush TextBrush = new SolidBrush(Color.White);
                        PointF TextStartPoint = new PointF(-CurrentFace.Width / 2, -CurrentFace.Height / 2);

                        Pen pen = new Pen(Color.Gray, 5);
                        gr.DrawEllipse(pen, -CurrentFace.Width / 2, -CurrentFace.Height / 2, CurrentFace.Width, CurrentFace.Height);

                        if ((programState == ProgramState.psChecked) && CurrentFace.isChecking)
                        {
                            pen = new Pen(Color.Green, 4);
                            gr.DrawEllipse(pen, -CurrentFace.Width / 2, -CurrentFace.Height / 2, CurrentFace.Width, CurrentFace.Height);
                            gr.DrawString("LIVE", TextFont, TextBrush, TextStartPoint);
                        }

                        if ((programState == ProgramState.psChecking) && CurrentFace.isChecking)
                        {
                            pen = new Pen(Color.Red, 2);
                            gr.DrawEllipse(pen, -CurrentFace.Width / 2, -CurrentFace.Height / 2, CurrentFace.Width, CurrentFace.Height);
                            gr.DrawString(CommandName(CurrentFace.StageNumber + 1, CommandsToDo[CurrentFace.StageNumber]), TextFont, TextBrush, TextStartPoint);

                            if (CheckCommand((int)CommandsToDo[CurrentFace.StageNumber], tracker.Handle, faceId, CurrentFace))
                            {
                                CurrentFace.StageNumber++;
                            }

                            if (CurrentFace.StageNumber == STAGES_NUMBER)
                            {
                                programState = ProgramState.psChecked;
                            }
                        }

                        if ((programState == ProgramState.psToCheck) && IsInside(mouseImg, CurrentFace.Center, CurrentFace.Width, CurrentFace.Height, CurrentFace.Angle))
                        {
                            programState = ProgramState.psChecking;
                            CurrentFace.isChecking = true;
                            CurrentFace.StageNumber = 0;

                            CurrentFace.state = (int)Command.LookStraight;
                            CommandsToDo[0] = Command.LookStraight;
                            Random CommandNumber = new Random();
                            bool SmileInList = false;
                            for (int j = 1; j < STAGES_NUMBER; ++j)
                            {
                                CommandsToDo[j] = Commands[CommandNumber.Next(Commands.Length)];
                                if (CommandsToDo[j] == Command.Smile)
                                    SmileInList = true;
                            }
                            if (!SmileInList)
                            {
                                CommandsToDo[CommandNumber.Next(STAGES_NUMBER - 1) + 1] = Command.Smile;
                            }
                        }

                        if ((programState == ProgramState.psNormal) && IsInside(mouseImg, CurrentFace.Center, CurrentFace.Width, CurrentFace.Height, CurrentFace.Angle))
                        {
                            pen = new Pen(Color.LightGreen, 2);
                            gr.DrawEllipse(pen, -CurrentFace.Width / 2, -CurrentFace.Height / 2, CurrentFace.Width, CurrentFace.Height);
                            gr.DrawString("Press to check liveness", TextFont, TextBrush, TextStartPoint);
                        }

                        gr.RotateTransform(-CurrentFace.Angle * 180 / (float)Math.PI);
                        gr.TranslateTransform(-CurrentFace.Center.X, -CurrentFace.Center.Y);
                    }
                }

                if (programState == ProgramState.psToCheck)
                    programState = ProgramState.psNormal;

                // Save the current frame and redraw pictureBox1
                if (currentFrame != null)
                {
                    var old = currentFrame;
                    currentFrame = null;
                    old.Dispose();
                }

                currentFrame = frameImage;
                pictureBox1.Invalidate();
                frame.Dispose();
                GC.Collect(); // collect the garbage after the deletion
            }
            camera.Close();

            tracker.Dispose();
            tracker = null;

            FSDK.FinalizeCapturing();
            FSDK.FinalizeLibrary();
        }

        /// <summary>
        /// Converts mouse coordinates from pictureBox1 coordinate system to image coordinate system
        /// Accounts for scaling and offset when displaying the image
        /// </summary>
        private void UpdateMouseImageCoords()
        {
            if (currentFrame == null) { mouseImg.X = 0; mouseImg.Y = 0; return; }

            int imgW = currentFrame.Width;
            int imgH = currentFrame.Height;
            int boxW = pictureBox1.Width;
            int boxH = pictureBox1.Height;
            float ratio = Math.Min((float)boxW / imgW, (float)boxH / imgH);
            int drawW = (int)(imgW * ratio);
            int drawH = (int)(imgH * ratio);
            int offsetX = (boxW - drawW) / 2;
            int offsetY = (boxH - drawH) / 2;

            // If the mouse is outside the image, coordinates are invalid
            if (mouse.X < offsetX || mouse.X >= offsetX + drawW || mouse.Y < offsetY || mouse.Y >= offsetY + drawH)
            {
                mouseImg.X = -1;
                mouseImg.Y = -1;
            }
            else
            {
                mouseImg.X = (int)((mouse.X - offsetX) / ratio);
                mouseImg.Y = (int)((mouse.Y - offsetY) / ratio);
            }
        }

        /// <summary>
        /// Handler for pictureBox1 paint event
        /// Displays the current frame preserving aspect ratio
        /// </summary>
        private void pictureBox1_Paint(object sender, PaintEventArgs e)
        {
            if (currentFrame == null)
                return;

            var pb = pictureBox1;
            var g = e.Graphics;
            g.Clear(pb.BackColor);

            int imgW = currentFrame.Width;
            int imgH = currentFrame.Height;
            int boxW = pb.Width;
            int boxH = pb.Height;

            float ratio = Math.Min((float)boxW / imgW, (float)boxH / imgH);
            int drawW = (int)(imgW * ratio);
            int drawH = (int)(imgH * ratio);
            int offsetX = (boxW - drawW) / 2;
            int offsetY = (boxH - drawH) / 2;

            g.DrawImage(currentFrame, new Rectangle(offsetX, offsetY, drawW, drawH));
        }

        /// <summary>
        /// Handler for mouse button release event on pictureBox1
        /// Manages transitions between liveness check states
        /// </summary>
        private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
        {
            if ((programState == ProgramState.psChecking) || (programState == ProgramState.psChecked))
            {
                programState = ProgramState.psNormal;
            }
            else if (programState == ProgramState.psNormal)
            {
                programState = ProgramState.psToCheck;
            }
        }

        /// <summary>
        /// Handler for mouse move event on pictureBox1
        /// Saves current mouse cursor coordinates
        /// </summary>
        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            mouse.X = e.X;
            mouse.Y = e.Y;
        }

        /// <summary>
        /// Handler for mouse cursor leaving pictureBox1 event
        /// Resets mouse coordinates
        /// </summary>
        private void pictureBox1_MouseLeave(object sender, EventArgs e)
        {
            mouse.X = 0;
            mouse.Y = 0;
        }
    }
}
