<script lang="ts">
import { defineComponent, type UnwrapRef, reactive, markRaw, toRaw } from 'vue';
import { fabric } from 'fabric';
import { PlusSquareOutlined, CloseOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons-vue';
import OpenposeObjectPanel from './components/OpenposeObjectPanel.vue';
import Header from './components/Header.vue';
import {
  OpenposePerson,
  OpenposeBody,
  OpenposeHand,
  OpenposeFace,
  OpenposeKeypoint2D,
  OpenposeObject,
  type IOpenposeJson,
  OpenposeBodyPart,
  OpenposeAnimal,
} from './Openpose';
import type { UploadFile } from 'ant-design-vue';
import LockSwitch from './components/LockSwitch.vue';
import _ from 'lodash';
import CryptoJS from 'crypto-js';

interface LockableUploadFile extends UploadFile {
  locked: boolean;
  scale: number;
};

interface AppData {
  canvasHeight: number;
  canvasWidth: number;

  personName: string;
  hideInvisibleKeypoints: boolean;
  people: Map<number, OpenposePerson>;
  keypointMap: Map<number, UnwrapRef<OpenposeKeypoint2D>>,
  canvas: fabric.Canvas | null;
  openposeCanvas: fabric.Rect;

  // Fields for uploaded background images.
  uploadedImageList: LockableUploadFile[];
  canvasImageMap: Map<string, fabric.Image>;

  // The corresponding OpenposePerson that the user has the collapse element
  // expanded.
  activePersonId: string | undefined;
  // The corresponding OpenposeObject(Hand/Face) that the user has the
  // collapse element expanded.
  activeBodyPart: OpenposeBodyPart | undefined;

  // The modal id to post message back to.
  modalId: string | undefined;
};

/**
 * The frame message from the main frame (ControlNet).
 */
interface IncomingFrameMessage {
  modalId: string;
  imageURL: string | undefined;
  poseURL: string;
};

interface OutgoingFrameMessage {
  modalId: string;
  poseURL: string;
};

const default_body_keypoints: [number, number, number][] = [
  [241, 77], [241, 120], [191, 118], [177, 183],
  [163, 252], [298, 118], [317, 182], [332, 245],
  [225, 241], [213, 359], [215, 454], [270, 240],
  [282, 360], [286, 456], [232, 59], [253, 60],
  [225, 70], [260, 72]
].map(loc => [loc[0], loc[1], 1.0]);

const default_left_hand_keypoints: [number, number, number][] = [
  [
    72.0,
    138.6749968987715,
    1
  ],
  [
    50.00001525878906,
    126.6749968987715,
    1
  ],
  [
    26.000015258789062,
    109.6749968987715,
    1
  ],
  [
    15.0,
    89.6749968987715,
    1
  ],
  [
    0.001,
    74.6749968987715,
    1
  ],
  [
    46.88441843878036,
    68.06475585206891,
    1
  ],
  [
    44.700867221123644,
    41.31155552497435,
    1
  ],
  [
    42.99998474121094,
    22.714115786649984,
    1
  ],
  [
    42.00298865302648,
    7.0136598257858935,
    1
  ],
  [
    66.64955876004365,
    63.25333859753661,
    1
  ],
  [
    65.00001525878906,
    40.6749968987715,
    1
  ],
  [
    65.94870679770906,
    22.18139755296059,
    1
  ],
  [
    65.38117571016846,
    0.001,
    1
  ],
  [
    82.28699289317831,
    68.16587713769567,
    1
  ],
  [
    84.50425981167291,
    45.869876375420176,
    1
  ],
  [
    85.4529971269601,
    27.623076702514766,
    1
  ],
  [
    85.02609712479875,
    9.122938432072758,
    1
  ],
  [
    98.0260818660098,
    76.01283565581218,
    1
  ],
  [
    103.05127794350199,
    60.272436637095865,
    1
  ],
  [
    107.35044123995635,
    46.921796571676936,
    1
  ],
  [
    110.70086722112376,
    31.519236310001304,
    1
  ]
];

const default_right_hand_keypoints: [number, number, number][] = [
  [
    37.00000762939453,
    140.03029482565358,
    1
  ],
  [
    59.000003814697266,
    132.03029482565358,
    1
  ],
  [
    83.00000381469727,
    117.0302948256536,
    1
  ],
  [
    99.99999618530273,
    99.0302948256536,
    1
  ],
  [
    117.99999618530273,
    88.0302948256536,
    1
  ],
  [
    68.60503479194651,
    69.66265371825791,
    1
  ],
  [
    72.0000114440918,
    51.0302948256536,
    1
  ],
  [
    75.99999618530273,
    34.0302948256536,
    1
  ],
  [
    80.00000381469727,
    17.0302948256536,
    1
  ],
  [
    47.878141976595,
    66.66265371825791,
    1
  ],
  [
    49.424375419378265,
    45.4861751947855,
    1
  ],
  [
    51.0,
    21.0302948256536,
    1
  ],
  [
    54.0,
    0.001,
    1
  ],
  [
    29.575632210016238,
    70.81461384130189,
    1
  ],
  [
    30.0,
    44.574414456521666,
    1
  ],
  [
    30.848747024059264,
    26.150394518043655,
    1
  ],
  [
    34.57563602471356,
    7.270494210433782,
    1
  ],
  [
    11.000003814697266,
    78.99843439499973,
    1
  ],
  [
    8.000003814697266,
    61.0302948256536,
    1
  ],
  [
    4.000007629394531,
    48.0302948256536,
    1
  ],
  [
    0.001,
    34.0302948256536,
    1
  ]
];

const default_face_keypoints: [number, number, number][] = [];

// identity_metrics * point == point.
const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];

function parseDataURLtoJSON(dataURL: string): any {
  const data = dataURL.split(',')[1]; // Extract the data portion
  const decodedData = atob(data); // Decode the data
  const json = JSON.parse(decodedData); // Parse the decoded data as JSON
  return json;
}

function serializeJSONtoDataURL(data: any): string {
  return "data:application/json;base64," + btoa(JSON.stringify(data));
}

async function calculateHash(s: string): Promise<string> {
  return CryptoJS.SHA256(s).toString()
}

function getImageDimensionsFromDataURL(dataURL: string): Promise<[number, number]> {
  return new Promise((resolve, reject) => {
    const img = new Image();

    img.onload = function () {
      const width = img.naturalWidth;
      const height = img.naturalHeight;
      resolve([width, height]);
    };

    img.onerror = function () {
      reject(new Error('Failed to load image.'));
    };

    img.src = dataURL;
  });
}

export default defineComponent({
  data(): AppData {
    return {
      canvasHeight: 512,
      canvasWidth: 512,
      personName: '',
      hideInvisibleKeypoints: false,
      people: new Map<number, OpenposePerson>(),
      canvas: null,
      openposeCanvas: new fabric.Rect({
        fill: "#000",
        selectable: false,
        evented: false,
      }),
      keypointMap: new Map<number, UnwrapRef<OpenposeKeypoint2D>>(),
      uploadedImageList: [],
      canvasImageMap: new Map<string, fabric.Image>(),
      activePersonId: undefined,
      activeBodyPart: undefined,
      modalId: undefined,
    };
  },
  setup() {
    return { OpenposeBodyPart };
  },
  mounted() {
    this.$nextTick(() => {
      this.canvas = markRaw(new fabric.Canvas(<HTMLCanvasElement>this.$refs.editorCanvas, {
        backgroundColor: '#222222',
        preserveObjectStacking: true,
        fireRightClick: true,
        stopContextMenu: true,
      }));

      const htmlCanvasWidth = Math.round((window.innerWidth * 16 / 24) * 0.95);
      const htmlCanvasHeight = Math.round(window.innerHeight * 0.95);
      this.resizeHTMLCanvas(htmlCanvasWidth, htmlCanvasHeight);
      this.canvas.add(this.openposeCanvas);
      // The openpose canvas should be at last layer.
      this.canvas.moveTo(this.openposeCanvas, 0);
      this.resizeOpenposeCanvas(this.canvasWidth, this.canvasHeight);

      // By default have a example person.
      this.addDefaultPerson();

      const selectionHandler = (event: fabric.IEvent<MouseEvent>) => {
        if (event.selected) {
          event.selected
            .filter(o => o instanceof OpenposeKeypoint2D)
            .forEach(p => {
              const proxy = this.getKeypointProxy(p as OpenposeKeypoint2D);
              if (event.selected!.length > 1)
                proxy.selected_in_group = true;
              proxy.selected = true;
            });
        }

        if (event.deselected) {
          event.deselected
            .filter(o => o instanceof OpenposeKeypoint2D)
            .forEach(p => {
              const proxy = this.getKeypointProxy(p as OpenposeKeypoint2D);
              if (event.deselected!.length > 1)
                proxy.selected_in_group = false;
              proxy.selected = false;
            });
        }
      }

      const keypointMoveHandler = (event: fabric.IEvent<MouseEvent>) => {
        if (event.target === undefined)
          return;

        const target = event.target;
        if (target instanceof fabric.ActiveSelection) {
          // Group of points movement.
          const t = target.calcTransformMatrix();
          target.forEachObject(obj => {
            if (obj instanceof OpenposeKeypoint2D) {
              obj.updateConnections(t);
            }
          });
        } else if (target instanceof OpenposeKeypoint2D) {
          // Single keypoint movement.
          target.updateConnections(IDENTITY_MATRIX);
          this.updateKeypointProxy(target);
        }
        this.canvas?.renderAll();
      };

      const hideKeypointHandler = (event: fabric.IEvent<MouseEvent>) => {
        // Only handles right click events.
        if (event.button !== 3) return;
        if (!(event.target instanceof OpenposeKeypoint2D)) return;
        event.target._visible = false;

        this.canvas?.renderAll();
      };

      this.canvas.on('object:moving', keypointMoveHandler);
      this.canvas.on('object:scaling', keypointMoveHandler);
      this.canvas.on('object:rotating', keypointMoveHandler);
      this.canvas.on('selection:created', selectionHandler);
      this.canvas.on('selection:cleared', selectionHandler);
      this.canvas.on('selection:updated', selectionHandler);
      this.canvas.on('mouse:down', hideKeypointHandler);

      // Zoom handler.
      this.canvas.on('mouse:wheel', (opt: fabric.IEvent<WheelEvent>) => {
        const delta = opt.e.deltaY;
        let zoom = this.canvas!.getZoom();
        zoom *= 0.999 ** delta;
        if (zoom > 20) zoom = 20;
        if (zoom < 0.01) zoom = 0.01;
        this.canvas!.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY } as fabric.Point, zoom);
        opt.e.preventDefault();
        opt.e.stopPropagation();
      });

      // Panning handler.
      let panning = false;
      let panningEnabled = false;
      // Enable panning by press SPACE or F because some users report SPACE
      // scrolls the iframe despite `e.preventDefault` is called. Issue #7.
      // Add keydown event to document
      document.addEventListener('keydown', (e) => {
        if (e.code === 'Space' || e.code === 'KeyF') {
          panningEnabled = true;
          this.canvas!.selection = false;
          // Prevent default behaviour of Space which is scroll the page down.
          e.preventDefault();
        }
      });

      // Add keyup event to document
      document.addEventListener('keyup', (e) => {
        if (e.code === 'Space' || e.code === 'KeyF') {
          panningEnabled = false;
          this.canvas!.selection = true;
        }
      });

      // Attach the mouse down event to start panning
      this.canvas.on('mouse:down', (opt: fabric.IEvent) => {
        if (panningEnabled) {
          panning = true;
        }
      });

      // Attach the mouse move event for panning
      this.canvas.on('mouse:move', (opt: fabric.IEvent<MouseEvent>) => {
        if (panning && opt && opt.e) {
          const delta = new fabric.Point(opt.e.movementX, opt.e.movementY);
          this.canvas!.relativePan(delta);
        }
      });

      // Attach the mouse up event to stop panning
      this.canvas.on('mouse:up', () => {
        panning = false;
      });

      // Handle incoming frame message.
      window.addEventListener('message', (event) => {
        const message = event.data as IncomingFrameMessage;
        if (_.some([message.modalId, message.poseURL], o => o === undefined)) {
          console.debug(`Unrecognized frame message received: ${JSON.stringify(message)}.`);
          return;
        }
        this.loadCanvasFromFrameMessage(message);
      });

      // Inform the parent frame that iframe is ready to receive message.
      if (window.self != window.top) {
        window.parent.postMessage({
          ready: true
        }, '*');
      }
    });
  },
  methods: {
    getKeypointProxy(keypoint: OpenposeKeypoint2D): UnwrapRef<OpenposeKeypoint2D> {
      return this.keypointMap.get(keypoint.id)!;
    },
    updateKeypointProxy(keypoint: OpenposeKeypoint2D) {
      const proxy = this.getKeypointProxy(keypoint);

      proxy.x = keypoint.x;
      proxy.y = keypoint.y;
    },
    addPerson(newPerson: OpenposePerson) {
      this.people.set(newPerson.id, newPerson);
      newPerson.addToCanvas(this.openposeCanvas);
      // Add the reactive keypoints to the keypointMap
      newPerson.allKeypoints().forEach((keypoint) => {
        this.keypointMap.set(keypoint.id, reactive(keypoint));
      });
      this.canvas?.renderAll();
    },
    addDefaultPerson() {
      const newPerson = new OpenposePerson(null, new OpenposeBody(default_body_keypoints));
      this.addPerson(newPerson);
    },
    removePerson(person: OpenposePerson) {
      // If the person is active right now, deactivate it.
      if (person.id.toString() === this.activePersonId) {
        this.updateActivePerson(undefined);
      }

      this.people.delete(person.id);
      person.removeFromCanvas();
      // Remove the reactive keypoints from the keypointMap
      person.allKeypoints().forEach((keypoint) => {
        this.keypointMap.delete(keypoint.id);
      });
      this.canvas?.renderAll();
    },
    addDefaultObject(person: OpenposePerson, part: OpenposeBodyPart) {
      let target: OpenposeObject;
      switch (part) {
        case OpenposeBodyPart.LEFT_HAND:
          target = new OpenposeHand(default_left_hand_keypoints);
          break;
        case OpenposeBodyPart.RIGHT_HAND:
          target = new OpenposeHand(default_right_hand_keypoints);
          break;
        case OpenposeBodyPart.FACE:
          person.face = new OpenposeFace(default_face_keypoints);
          target = person.face;
          break;
      }
      this.addObject(person, part, target);
    },
    addObject(person: OpenposePerson, part: OpenposeBodyPart, target: OpenposeObject) {
      target.addToCanvas(this.openposeCanvas);
      target.keypoints.forEach(keypoint => {
        this.keypointMap.set(keypoint.id, reactive(keypoint));
      });

      switch (part) {
        case OpenposeBodyPart.LEFT_HAND:
          person.attachLeftHand(target as OpenposeHand);
          break;
        case OpenposeBodyPart.RIGHT_HAND:
          person.attachRightHand(target as OpenposeHand);
          break;
        case OpenposeBodyPart.FACE:
          person.attachFace(target as OpenposeFace);
          break;
      }
      this.canvas?.renderAll();
    },
    removeObject(person: OpenposePerson, part: OpenposeBodyPart) {
      let target: OpenposeObject | undefined;
      switch (part) {
        case OpenposeBodyPart.LEFT_HAND:
          target = person.left_hand;
          person.left_hand = undefined;
          break;
        case OpenposeBodyPart.RIGHT_HAND:
          target = person.right_hand;
          person.right_hand = undefined;
          break;
        case OpenposeBodyPart.FACE:
          target = person.face;
          person.face = undefined;
          break;
      }

      if (!target) return;

      target.removeFromCanvas();
      target.keypoints.forEach(keypoint => {
        this.keypointMap.delete(keypoint.id);
      });
      if (this.activeBodyPart === part) {
        this.updateActiveBodyPart(undefined, person);
      }
      this.canvas?.renderAll();
    },
    // HTML canvas is the real canvas object.
    resizeHTMLCanvas(newWidth: number, newHeight: number) {
      if (!this.canvas)
        return;
      this.canvas.setWidth(newWidth);
      this.canvas.setHeight(newHeight);
      this.canvas.calcOffset();
      this.canvas.requestRenderAll();
    },
    // Openpose canvas is a rect bound on the HTML canvas.
    resizeOpenposeCanvas(newWidth: number, newHeight: number) {
      if (!this.canvas)
        return;
      this.openposeCanvas.set({
        width: newWidth,
        height: newHeight,
      });
      this.canvas.centerObject(this.openposeCanvas);
      this.openposeCanvas.setCoords();
      this.canvas.requestRenderAll();
    },
    onLockedChange(file: LockableUploadFile, locked: boolean) {
      file.locked = locked;

      const img = this.canvasImageMap.get(file.uid);
      if (!img) return;

      if (locked) {
        if (this.canvas?.getActiveObjects().includes(img)) {
          this.canvas.discardActiveObject();
        }
        img.set({
          selectable: false,
          evented: false,
          hasControls: false,
          hasBorders: false,
        });
      } else {
        img.set({
          selectable: true,
          evented: true,
          hasControls: true,
          hasBorders: true,
        });
      }
      this.canvas?.renderAll();
    },
    updateActivePerson(activePersonId: string | undefined) {
      if (this.activePersonId === activePersonId) return;

      if (!activePersonId) {
        // Collapse current panel.
        // If there is a body part panel expanded, collapse that as well.
        const activePerson = this.people.get(parseInt(this.activePersonId!))!;
        this.updateActiveBodyPart(undefined, activePerson);
      }

      this.activePersonId = activePersonId;
    },
    updateActiveBodyPart(activeBodyPart: OpenposeBodyPart | undefined, person: OpenposePerson) {
      if (this.activeBodyPart === activeBodyPart) return;

      if (activeBodyPart === undefined) {
        if (this.activeBodyPart !== undefined) {
          // There can only be one active person. If we collapse the person panel
          // or collapse the body part panel. This function can still receive the 
          // correct person of the targeted object.
          const target = person[this.activeBodyPart]!;
          if (target)
            target.grouped = true;
        }
      } else {
        const target = person[activeBodyPart]!;
        target.grouped = true;
        // Ungroup the object so that user can operate on each individual keypoint.
        target.grouped = false;
      }
      this.activeBodyPart = activeBodyPart;
    },
    resetZoom() {
      if (!this.canvas) return;
      this.canvas.setViewportTransform(IDENTITY_MATRIX);
    },
    /**
     * Adjust canvas zoom level to fit the target group's bounding box.
     * @param group The group object to zoom into.
     * @param zoomed_size: How big should the group take the space of the zoomed canvas.
     */
    zoomToGroup(group: fabric.Group, zoomed_size: number = 1.0) {
      if (!this.canvas) return;

      // Get the bounding rectangle of the group
      const boundingRect = group.getBoundingRect();

      // Calculate the scale factor
      const scaleFactor = Math.min(
        this.canvas.getWidth() / boundingRect.width,
        this.canvas.getHeight() / boundingRect.height
      );

      // Set the zoom level and center the viewport
      const centerX = boundingRect.left + boundingRect.width / 2;
      const centerY = boundingRect.top + boundingRect.height / 2;
      const center = new fabric.Point(centerX, centerY);
      this.canvas.zoomToPoint(center, scaleFactor * zoomed_size);
    },
    handleBeforeUploadImage(file: Blob) {
      const reader = new FileReader();
      reader.onload = (e) => {
        this.loadBackgroundImageFromURL(e.target!.result! as string);
      };
      reader.readAsDataURL(file);

      // Return false to prevent the default upload behavior
      return false;
    },
    loadBackgroundImageFromURL(url: string) {
      fabric.Image.fromURL(url, (img) => {
        img.set({
          left: this.openposeCanvas.left,
          top: this.openposeCanvas.top,
          scaleX: 1.0,
          scaleY: 1.0,
          opacity: 0.5,
          hasControls: true,
          hasBorders: true,
          lockScalingX: false,
          lockScalingY: false,
        });

        this.canvas?.add(img);
        // Image should not block skeleton.
        this.canvas?.moveTo(img, 1);
        this.canvas?.renderAll();

        const uploadFile = this.uploadedImageList[this.uploadedImageList.length - 1];
        uploadFile.locked = false;
        uploadFile.scale = 1.0;
        this.canvasImageMap.set(uploadFile.uid, img);
      });
    },
    isImage(file: UploadFile) {
      return /\.(jpeg|jpg|gif|png|bmp)$/i.test(file.name);
    },
    handleRemoveImage(image: UploadFile) {
      if (!this.canvasImageMap.has(image.uid)) return;

      this.canvas?.remove(toRaw(this.canvasImageMap.get(image.uid)!));
      this.canvas?.renderAll();
    },
    scaleImage(image: LockableUploadFile, scale: number) {
      image.scale = scale;
      if (!this.canvasImageMap.has(image.uid)) return;

      const img = this.canvasImageMap.get(image.uid)!;
      img.set({
        scaleX: scale,
        scaleY: scale,
      });

      this.canvas?.renderAll();
    },
    readOpenposeJson(file: Blob): Promise<IOpenposeJson> {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = (e) => {
          let poseJson: IOpenposeJson;
          try {
            poseJson = JSON.parse(e.target!.result! as string) as IOpenposeJson;
          } catch (ex: any) {
            reject(ex);
            return;
          }
          resolve(poseJson);
        };
        reader.readAsText(file);
      });
    },
    parseOpenposeJson(poseJson: IOpenposeJson): OpenposePerson[] {
      function preprocessPoints(nums: number[], canvasWidth: number, canvasHeight: number): [number, number, number][] {
        const normalized = _.every(nums, num => Math.abs(num) <= 1.0);
        const xFactor = normalized ? canvasWidth : 1.0;
        const yFactor = normalized ? canvasHeight : 1.0;
        const points = _.chunk(nums, 3) as [number, number, number][];
        return points.map(p => [p[0] * xFactor, p[1] * yFactor, p[2]]);
      }
      const canvasHeight = poseJson.canvas_height;
      const canvasWidth = poseJson.canvas_width;


      return (poseJson.people || []).map((personJson): OpenposePerson | undefined => {
        const body = OpenposeBody.create(preprocessPoints(personJson.pose_keypoints_2d, canvasWidth, canvasHeight));
        if (body === undefined) {
          // If body is malformatted, no need to render face/hand.
          return undefined;
        }
        return new OpenposePerson(null,
          body,
          personJson.hand_left_keypoints_2d ?
            OpenposeHand.create(preprocessPoints(personJson.hand_left_keypoints_2d, canvasWidth, canvasHeight)) : undefined,
          personJson.hand_right_keypoints_2d ?
            OpenposeHand.create(preprocessPoints(personJson.hand_right_keypoints_2d, canvasWidth, canvasHeight)) : undefined,
          personJson.face_keypoints_2d ?
            OpenposeFace.create(preprocessPoints(personJson.face_keypoints_2d, canvasWidth, canvasHeight)) : undefined,
        )
      }).concat(
        (poseJson.animals || []).map((animal): OpenposePerson | undefined => {
          const openposeAnimal = OpenposeAnimal.create(preprocessPoints(animal, canvasWidth, canvasHeight))
          return openposeAnimal ? new OpenposePerson(null, openposeAnimal) : undefined;
        })
      ).filter(person => person !== undefined) as OpenposePerson[];
    },
    /**
     * Adds a body part from the given JSON file.
     * The given JSON is expected to only have one person.
     * @param file JSON file blob
     * @param person 
     * @param part 
     */
    addJsonObject(file: Blob, person: OpenposePerson, part: OpenposeBodyPart) {
      this.readOpenposeJson(file).then(this.parseOpenposeJson).then((people: OpenposePerson[]) => {
        if (people.length === 0) return;
        const firstPerson = people[0];
        switch (part) {
          case OpenposeBodyPart.LEFT_HAND:
            if (firstPerson.left_hand === undefined) {
              this.$notify({ title: 'Error', desc: 'Left hand does not exist in Json' });
              return;
            }
            this.addObject(person, part, firstPerson.left_hand);
            break;
          case OpenposeBodyPart.RIGHT_HAND:
            if (firstPerson.right_hand === undefined) {
              this.$notify({ title: 'Error', desc: 'Right hand does not exist in Json' });
              return;
            }
            this.addObject(person, part, firstPerson.right_hand);
            break;
          case OpenposeBodyPart.FACE:
            if (firstPerson.face === undefined) {
              this.$notify({ title: 'Error', desc: 'Face does not exist in Json' });
              return;
            }
            this.addObject(person, part, firstPerson.face);
            break;
        }
      }).catch((ex) => this.$notify(ex + ''));
      return false;
    },
    /**
     * Adds all people specified in the given JSON file to canvas.
     * @param file JSON file blob
     */
    handleBeforeUploadJson(file: Blob) {
      this.readOpenposeJson(file)
        .then(this.loadPeopleFromJson)
        .catch((ex) => this.$notify(ex + ''));
      return false;
    },
    loadPeopleFromJson(poseJson: IOpenposeJson) {
      const canvasHeight = poseJson.canvas_height;
      const canvasWidth = poseJson.canvas_width;
      this.canvasHeight = _.max([canvasHeight, this.canvasHeight])!;
      this.canvasWidth = _.max([canvasWidth, this.canvasWidth])!;
      this.resizeOpenposeCanvas(this.canvasWidth, this.canvasHeight);
      this.parseOpenposeJson(poseJson).forEach(person => this.addPerson(person));
    },
    /**
     * Clear everything on the canvas.
     */
    clearCanvas() {
      // Remove all people.
      [...this.people.values()].forEach(person => {
        this.removePerson(person);
      });
      // Remove all background images.
      this.uploadedImageList.forEach(image => {
        this.handleRemoveImage(image);
      });
      this.uploadedImageList.splice(0); // Clear `uploadedImageList`.
      this.resetZoom();
    },
    loadCanvasFromRequestParams() {
      this.clearCanvas();
      const data = window.dataFromServer;
      if (_.isEmpty(data)) {
        return;
      }

      let poseJson: IOpenposeJson;
      try {
        poseJson = JSON.parse(data.pose) as IOpenposeJson;
      } catch (ex: any) {
        this.$notify({ title: 'Error', desc: ex.message });
        return;
      }
      this.loadPeopleFromJson(poseJson);
      this.loadBackgroundImageFromURL(data.image_url);
    },
    async loadCanvasFromFrameMessage(message: IncomingFrameMessage) {
      this.modalId = message.modalId;

      this.clearCanvas();
      const openposeJson = parseDataURLtoJSON(message.poseURL) as IOpenposeJson;
      this.canvasHeight = openposeJson.canvas_height;
      this.canvasWidth = openposeJson.canvas_width;
      this.loadPeopleFromJson(openposeJson);

      // (Optional) Loads background image.
      if (message.imageURL) {
        const imageFile = {
          locked: false,
          scale: 1.0,
          name: 'controlnet input',
          uid: await calculateHash(message.imageURL),
        } as LockableUploadFile;
        this.uploadedImageList.push(imageFile);
        this.loadBackgroundImageFromURL(message.imageURL);
        const [imgWidth, imgHeight] = await getImageDimensionsFromDataURL(message.imageURL);
        this.scaleImage(imageFile, Math.min(this.canvasHeight / imgHeight, this.canvasWidth / imgWidth));
        imageFile.locked = true;
        this.onLockedChange(imageFile, true);
      }
    },
    getCanvasAsOpenposeJson(): IOpenposeJson {
      return {
        people: [...this.people.values()]
          .filter(person => !person.allKeypointsInvisible() && !person.isAnimal)
          .map(person => person.toJson()),
        animals: [...this.people.values()]
          .filter(person => !person.allKeypointsInvisible() && person.isAnimal)
          .map(person => person.toJson()),
        canvas_width: this.canvasWidth,
        canvas_height: this.canvasHeight,
      } as IOpenposeJson;
    },
    sendCanvasAsFrameMessage() {
      if (this.modalId === undefined) return;
      window.parent.postMessage({
        modalId: this.modalId,
        poseURL: serializeJSONtoDataURL(this.getCanvasAsOpenposeJson()),
      } as OutgoingFrameMessage, '*');
    },
    downloadCanvasAsJson() {
      const link = document.createElement('a');
      link.href = serializeJSONtoDataURL(this.getCanvasAsOpenposeJson());
      link.download = 'pose.json';
      link.click();
    },
    downloadCanvasAsImage() {
      if (!this.canvas) return;
      this.resetZoom();

      // Get the data URL of the canvas as a PNG image
      const dataUrl = this.canvas.toDataURL({ format: 'image/png' });

      // Crop the image.
      const newCanvas = new fabric.StaticCanvas(null, {
        width: this.canvasWidth,
        height: this.canvasHeight,
      });

      fabric.Image.fromURL(dataUrl, img => {
        img.set({
          left: -this.openposeCanvas.left!,
          top: -this.openposeCanvas.top!,
        });
        newCanvas.add(img);
        newCanvas.renderAll();

        const croppedImageUrl = newCanvas.toDataURL({ format: 'image/png' });

        // Create an img element with the data URL
        const imgElem = document.createElement('img');
        imgElem.src = croppedImageUrl;

        // Create a link element with the data URL
        const link = document.createElement('a');
        link.href = croppedImageUrl;
        link.download = 'pose.png';
        // Trigger a click event on the link to initiate the download
        link.click();
      });
    },
  },
  components: {
    PlusSquareOutlined,
    CloseOutlined,
    UploadOutlined,
    DownloadOutlined,
    OpenposeObjectPanel,
    LockSwitch,
    Header,
  }
});
</script>

<template>
  <a-row>
    <a-col :span="8" id="control-panel">
      <Header></Header>
      <a-button v-if="modalId !== undefined" @click="sendCanvasAsFrameMessage">
        {{ $t('ui.sendPose') }}
      </a-button>
      <a-divider orientation="left" orientation-margin="0px">
        {{ $t('ui.keybinding') }}
      </a-divider>
      <a-descriptions :column="1">
        <a-descriptions-item :label="$t('ui.panningKeybinding')">{{ $t('ui.panningDescription') }}</a-descriptions-item>
        <a-descriptions-item :label="$t('ui.zoomKeybinding')">{{ $t('ui.zoomDescription') }}</a-descriptions-item>
        <a-descriptions-item :label="$t('ui.hideKeybinding')">{{ $t('ui.hideDescription') }}</a-descriptions-item>
      </a-descriptions>
      <a-divider orientation="left" orientation-margin="0px">
        {{ $t('ui.canvas') }}
      </a-divider>
      <div>
        <a-space>
          <a-input-number type="inputNumber" addon-before="Width" addon-after="px" v-model:value="canvasWidth" :min="64"
            :max="4096" />
          <a-input-number type="inputNumber" addon-before="Height" addon-after="px" v-model:value="canvasHeight" :min="64"
            :max="4096" />
          <a-button @click="resizeOpenposeCanvas(canvasWidth, canvasHeight)">{{ $t('ui.resizeCanvas') }}</a-button>
          <a-button @click="resetZoom()">{{ $t('ui.resetZoom') }}</a-button>
        </a-space>
      </div>
      <a-divider orientation="left" orientation-margin="0px">
        {{ $t('ui.backgroundImage') }}
      </a-divider>
      <a-upload v-model:file-list="uploadedImageList" list-type="picture" accept="image/*"
        :beforeUpload="handleBeforeUploadImage" @remove="handleRemoveImage">
        <a-button>
          <upload-outlined></upload-outlined>
          {{ $t('ui.uploadImage') }}
        </a-button>
        <template #itemRender="{ file, actions }">
          <a-card class="uploaded-file-item">
            <LockSwitch :locked="file.locked !== undefined ? file.locked : false"
              @update:locked="onLockedChange(file, $event)" />
            <img v-if="isImage(file)" :src="file.thumbUrl || file.url" :alt="file.name" class="image-thumbnail" />
            <span>{{ file.name }}</span>
            <a-input-number class="scale-ratio-input" addon-before="scale ratio" @update:value="scaleImage(file, $event)"
              :min="0" :value="file.scale !== undefined ? file.scale : 1.0" :precision="2" />
            <close-outlined @click="actions.remove" class="close-icon" />
          </a-card>
        </template>
      </a-upload>
      <a-divider orientation="left" orientation-margin="0px">
        {{ $t('ui.poseControl') }}
      </a-divider>
      <a-space>
        <a-button @click="addDefaultPerson">
          <plus-square-outlined />
          {{ $t('ui.addPerson') }}
        </a-button>
        <a-upload accept="application/json" :beforeUpload="handleBeforeUploadJson" :showUploadList="false">
          <a-button>
            <upload-outlined></upload-outlined>
            {{ $t('ui.uploadJSON') }}
          </a-button>
        </a-upload>
        <a-button @click="downloadCanvasAsJson">
          <download-outlined></download-outlined>
          {{ $t('ui.downloadJSON') }}
        </a-button>
        <a-button @click="downloadCanvasAsImage">
          <download-outlined></download-outlined>
          {{ $t('ui.downloadImage') }}
        </a-button>
      </a-space>
      <a-collapse accordion :activeKey="activePersonId" @update:activeKey="updateActivePerson">
        <OpenposeObjectPanel v-for="person in people.values()" :object="person.body" :display_name="person.name"
          @removeObject="removePerson(person)" :key="person.id">
          <template #extra-control>
            <!-- TODO: make this repetitive code a component. -->
            <div v-if="person.left_hand === undefined && !person.isAnimal">
              <a-button @click="addDefaultObject(person, OpenposeBodyPart.LEFT_HAND)">{{ $t('ui.addLeftHand')
              }}</a-button>
              <a-upload accept="application/json"
                :beforeUpload="(file: Blob) => addJsonObject(file, person, OpenposeBodyPart.LEFT_HAND)"
                :showUploadList="false">
                <a-button>
                  <upload-outlined></upload-outlined>
                </a-button>
              </a-upload>
            </div>
            <div v-if="person.right_hand === undefined && !person.isAnimal">
              <a-button @click="addDefaultObject(person, OpenposeBodyPart.RIGHT_HAND)">{{ $t('ui.addRightHand')
              }}</a-button>
              <a-upload accept="application/json"
                :beforeUpload="(file: Blob) => addJsonObject(file, person, OpenposeBodyPart.RIGHT_HAND)"
                :showUploadList="false">
                <a-button>
                  <upload-outlined></upload-outlined>
                </a-button>
              </a-upload>
            </div>
            <div v-if="person.face === undefined && !person.isAnimal">
              <a-upload accept="application/json"
                :beforeUpload="(file: Blob) => addJsonObject(file, person, OpenposeBodyPart.FACE)"
                :showUploadList="false">
                <a-button>
                  {{ $t('ui.addFace') }}
                  <upload-outlined></upload-outlined>
                </a-button>
              </a-upload>
            </div>
            <a-collapse accordion :activeKey="activeBodyPart" @update:activeKey="updateActiveBodyPart($event, person)">
              <OpenposeObjectPanel v-if="person.left_hand !== undefined" :object="person.left_hand"
                :display_name="'Left Hand'" @removeObject="removeObject(person, OpenposeBodyPart.LEFT_HAND)"
                :key="OpenposeBodyPart.LEFT_HAND" />
              <OpenposeObjectPanel v-if="person.right_hand !== undefined" :object="person.right_hand"
                :display_name="'Right Hand'" @removeObject="removeObject(person, OpenposeBodyPart.RIGHT_HAND)"
                :key="OpenposeBodyPart.RIGHT_HAND" />
              <OpenposeObjectPanel v-if="person.face !== undefined" :object="person.face" :display_name="'Face'"
                @removeObject="removeObject(person, OpenposeBodyPart.FACE)" :key="OpenposeBodyPart.FACE" />
            </a-collapse>
          </template>
        </OpenposeObjectPanel>
      </a-collapse>
    </a-col>

    <a-col :span="16" id="canvas-panel">
      <canvas ref="editorCanvas"></canvas>
    </a-col>
  </a-row>
</template>

<style>
.hidden {
  opacity: 50%;
  text-decoration: line-through;
}
</style> 