<template>
  <div class="svg-content">
    <div class="content" ref="root">
      <div class="video-scrolls">
        <VideoScroll v-for="scroll in scrolls" :definition="scroll"/>
      </div>
    </div>
    <AudioArea v-for="audio in audioAreas" :definition="audio" :bbox="bbox"/>
    <div class="devpanel">
      <div>
        Current position: {{ Math.round(bbox.x) }}x{{ Math.round(bbox.y) }}
      </div>
      <div>
        Zoom level: {{ currentTransform ? (Math.round(currentTransform.scale * 1000) / 1000) : '???' }}
      </div>
      <label>
        <input v-model="showInternal" type="checkbox">
        <label>Show internal elements</label>
      </label>
    </div>
  </div>
</template>

<script lang="ts">
import {defineComponent, onMounted, reactive, ref} from "vue";
import createPanZoom, {PanZoom, Transform} from "panzoom";
import VideoScroll, {VideoScrollDef, VideoScrollDirection} from "@/components/VideoScroll.vue";
import AudioArea, {AudioAreaDef} from "@/components/AudioArea.vue";
import Stats from "stats.js";

export default defineComponent({
  name: "SVGContent",
  components: {AudioArea, VideoScroll},
  props: {
    url: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      showInternal: false
    };
  },
  watch: {
    showInternal(value) {
      Array.from(this.root!.getElementsByClassName("internal")).forEach((el) => {
        (el as SVGElement).style.visibility = value ? "visible" : "hidden";
      });
    }
  },
  setup(props, {emit}) {
    const root = ref<HTMLDivElement | null>(null);
    const panzoom = ref<null | PanZoom>(null);
    const anchors = ref<SVGRectElement[]>([]);
    const scrolls = ref<VideoScrollDef[]>([]);
    const panToAnchor = ref();
    const audioAreas = ref<AudioAreaDef[]>([]);
    const currentTransform = ref<Transform | null>(null);
    const bbox = reactive({
      x: ref(0),
      y: ref(0),
      w: ref(0),
      h: ref(0)
    });

    onMounted(async () => {
      const element = root.value as unknown as HTMLDivElement;

      // Fetch & load SVG
      const fetchResult = await fetch(props.url);
      const svgParsed = new DOMParser().parseFromString(await fetchResult.text(), "image/svg+xml") as Document;
      const svg = element.appendChild(svgParsed.firstElementChild as Element) as any;

      // Set document background
      const pageColor = svg.getElementById("base")?.attributes.getNamedItem("pagecolor");
      if (pageColor) {
        emit("setBackground", pageColor.value);
      }

      // PanZoom
      const pz = createPanZoom(element, {
        smoothScroll: false,
        minZoom: 0.02,
        maxZoom: 3637937,
        zoomSpeed: 0.05,
        zoomDoubleClickSpeed: 1,
        onDoubleClick: function (e) {
          if (!document.fullscreenElement) {
            document.body.requestFullscreen();
          } else {
            document.exitFullscreen();
          }

          return true;
        }
      });
      panzoom.value = pz;
      currentTransform.value = pz.getTransform();

      // Calculate SVG-unit bounding box, update transform
      pz.on("transform", function (_) {
        const transform = pz.getTransform();
        const currentRatio = svg.clientWidth * transform.scale / svg.viewBox.baseVal.width;

        bbox.x = transform.x / currentRatio * -1;
        bbox.y = transform.y / currentRatio * -1;
        bbox.w = window.innerWidth / currentRatio;
        bbox.h = window.innerHeight / currentRatio;

        currentTransform.value = transform;
      });

      function panToElement(target: SVGRectElement, smooth: boolean) {
        const transform = pz.getTransform();
        const currentRatio = svg.clientWidth * transform.scale / svg.viewBox.baseVal.width;
        const ratio = svg.clientWidth / svg.viewBox.baseVal.width;
        const targetScale = window.innerWidth / (target.width.baseVal.value * ratio);

        const svgTargetX = (target.x.baseVal.value + target.width.baseVal.value / 2) * currentRatio;
        const svgTargetY = (target.y.baseVal.value + target.height.baseVal.value / 2) * currentRatio;

        if (smooth) {
          pz.smoothMoveTo(
              svgTargetX * -1 + window.innerWidth / 2,
              svgTargetY * -1 + window.innerHeight / 2,
          );

          setTimeout(() => {
            const finalTransform = pz.getTransform();
            pz.smoothZoomAbs(
                (svgTargetX + finalTransform.x),
                (svgTargetY + finalTransform.y),
                targetScale
            );
          }, 400 * 2);
        } else {
          pz.moveTo(
              svgTargetX * -1 + window.innerWidth / 2,
              svgTargetY * -1 + window.innerHeight / 2,
          );
          pz.zoomAbs(window.innerWidth / 2, window.innerHeight / 2, targetScale);
        }
      }

      // Pan to start element
      const start = processStart(svg);
      if (start) {
        panToElement(start, false);

        window.addEventListener("keydown", (ev) => {
          if (ev.key === " ") {
            panToElement(start, true);
          }
        });
      }

      // Edge scrolling
      const MOVE_EDGE = 75;
      const MAX_SPEED = 20;
      let edgeScrollInterval: number | undefined;
      window.addEventListener("mousemove", (ev) => {
        clearTimeout(edgeScrollInterval);
        edgeScrollInterval = undefined;
        if (!document.fullscreenElement) {
          return;
        }

        let horizontalShift: number;
        let verticalShift: number;

        const transform = pz.getTransform();
        if (ev.clientX < MOVE_EDGE || ev.clientX > window.innerWidth - MOVE_EDGE) {
          const horizontalEdgeDistance =
              (ev.clientX < window.innerWidth / 2) ? ev.clientX : (ev.clientX - window.innerWidth);
          const horizontalRatio = (MOVE_EDGE - Math.abs(horizontalEdgeDistance)) / MOVE_EDGE;
          horizontalShift = horizontalRatio * Math.sign(horizontalEdgeDistance) * MAX_SPEED;
        } else {
          horizontalShift = 0;
        }

        if (ev.clientY < MOVE_EDGE || ev.clientY > window.innerHeight - MOVE_EDGE) {
          const verticalEdgeDistance =
              (ev.clientY < window.innerHeight / 2) ? ev.clientY : (ev.clientY - window.innerHeight);
          const verticalRatio = (MOVE_EDGE - Math.abs(verticalEdgeDistance)) / MOVE_EDGE;
          verticalShift = verticalRatio * Math.sign(verticalEdgeDistance) * MAX_SPEED;
        } else {
          verticalShift = 0;
        }

        if (horizontalShift || verticalShift) {
          edgeScrollInterval = setInterval(() => {
            pz.moveTo(transform!.x + horizontalShift, transform!.y + verticalShift);
          }, 1000 / 60);
        }
      });

      // Anchors
      anchors.value = processAnchors(svg);
      panToAnchor.value = (anchor: SVGRectElement) => {
        panToElement(anchor, true);
      };

      // Audio areas
      audioAreas.value = processAudio(svg);

      // Videoscrolls
      scrolls.value = await processScrolls(svg);
    });

    if (process.env.VUE_APP_DEMO) {
      const stats = new Stats();
      document.body.appendChild(stats.dom);

      function noop() {
        stats.begin();
        stats.end();
        requestAnimationFrame(noop);
      }

      requestAnimationFrame(noop);
    }

    return {
      root,
      panzoom,
      anchors,
      panToAnchor,
      scrolls,
      audioAreas,
      bbox,
      currentTransform
    };
  },
});

function processAnchors(document: XMLDocument): SVGRectElement[] {
  let result = [];
  let i = 1;
  while (true) {
    let anchor = document.getElementById(`anchor_${i}`) as SVGRectElement | null;
    if (anchor === null) {
      break;
    }
    anchor.classList.add("internal");
    result.push(anchor);
    i++;
  }
  return result;
}

async function processScrolls(svg: XMLDocument): Promise<VideoScrollDef[]> {
  const ratio = (svg as any).clientWidth / (svg as any).viewBox.baseVal.width;

  return Promise.all(
      Array.from(svg.getElementsByTagName("image"))
          .filter((el) => Array.from(el.children).some((el) => el.tagName == "desc"))
          .map(async (el) => {
            const descNode = el.children[0]; // to fix
            const [directionString, filesURL] = descNode.textContent!.split("\n");

            if (!Object.values(VideoScrollDirection).includes(directionString as VideoScrollDirection)) {
              throw new Error("Unknown direction string.");
            }

            const fileFetch = await fetch(`content/${filesURL}`);
            const preURL = fileFetch.url.replace(/\/files.lst$/, "");
            const files = (await fileFetch.text()).split("\n").filter(Boolean).map((str) => `${preURL}/${str}`);

            return {
              top: el.y.baseVal.value * ratio,
              left: el.x.baseVal.value * ratio,
              width: el.width.baseVal.value * ratio,
              height: el.height.baseVal.value * ratio,
              direction: directionString as VideoScrollDirection,
              files
            };
          })
  );
}

function processAudio(svg: XMLDocument): AudioAreaDef[] {
  const ratio = (svg as any).clientWidth / (svg as any).viewBox.baseVal.width;

  return Array.from(svg.getElementsByTagName("circle"))
      .filter((el) => Array.from(el.children).some((el) => el.tagName == "desc"))
      .map((el) => {
        el.classList.add("internal");

        const descNode = el.children[0]; // to fix
        const audioSrc = descNode.textContent!.trim();

        return {
          cx: el.cx.baseVal.value,
          cy: el.cy.baseVal.value,
          radius: el.r.baseVal.value,
          src: `content/${audioSrc}`,
        };
      });
}

function processStart(svg: XMLDocument): SVGRectElement | null {
  const start = svg.getElementById("start");
  if (start) {
    start.classList.add("internal");
  }
  return start as (SVGRectElement | null);
}

</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
.svg-content svg {
  overflow: visible;
}

.svg-content svg .internal {
  visibility: hidden;
}

.devpanel {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 999;

  color: white;
  background: #000000aa;
  border: 2px solid white;
  font-family: monospace;
  padding: 1em 2em;
}
</style>
