<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>
        <span>Current viewport position:</span>
        <span>{{ Math.round(bbox.x) }}x{{ Math.round(bbox.y) }}</span>
      </div>
      <div>
        <span>Current cursor position:</span>
        <span>{{ Math.round(mousePosition.x) }}x{{ Math.round(mousePosition.y) }}</span>
      </div>
      <div>
        <span>Zoom level:</span><span>{{ (Math.round(bbox.z * 1000) / 1000) }}</span>
      </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} from "panzoom";
import VideoScroll, {VideoScrollDef, VideoScrollDirection} from "@/components/VideoScroll.vue";
import AudioArea, {AudioAreaDef} from "@/components/AudioArea.vue";
import Stats from "stats.js";
import {rotate} from "@/utils";


export interface BoundingBox {
  x: number,
  y: number,
  w: number,
  h: number,
  z: number
}


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 bbox: BoundingBox = reactive({
      x: ref(0),
      y: ref(0),
      w: ref(0),
      h: ref(0),
      z: ref(1)
    });
    const mousePosition = reactive({
      x: ref(0),
      y: ref(0)
    });
    const panning = ref(false);

    onMounted(async () => {
      const element = root.value as unknown as HTMLDivElement;
      console.info("[SVG] Initializing.");

      // Fetch & load SVG
      console.info(`[SVG] Fetching "${props.url}..."`);
      const fetchResult = await fetch(props.url);
      console.debug("[SVG] Fetched, parsing...");
      const svgParsed = new DOMParser().parseFromString(await fetchResult.text(), "image/svg+xml") as Document;
      console.debug("[SVG] Parsed.");
      const svg = element.appendChild(svgParsed.firstElementChild as Element) as any;

      // Set document background
      const pageColor = svg.getElementById("base")?.attributes.getNamedItem("pagecolor");
      if (pageColor) {
        console.debug(`[SVG] Found pageColor attribute: ${pageColor.value}`);
        emit("setBackground", pageColor.value);
      }

      // PanZoom
      const pz = createPanZoom(element, {
        smoothScroll: false,
        minZoom: 0.05,
        maxZoom: 3637937,
        zoomSpeed: 0.05,
        zoomDoubleClickSpeed: 1,
        beforeMouseDown: () => {
          return panning.value;
        },
        beforeWheel: () => {
          return panning.value;
        },
        onDoubleClick: () => {
          if (!document.fullscreenElement) {
            console.debug("[SVG] Fullscreen requested.");
            document.body.requestFullscreen();
          } else {
            console.debug("[SVG] Fullscreen exited.");
            document.exitFullscreen();
          }

          return true;
        }
      });
      panzoom.value = pz;

      // 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;
        bbox.z = transform.scale;
      });

      function panToElement(target: SVGRectElement, smooth: boolean) {
        console.debug(`[SVG] Panning to element: #${target.id}`);
        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) {
          panning.value = true;

          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
            );
            setTimeout(() => {
              panning.value = false;
            }, 400);
          }, 400 * 2);
        } else {
          pz.moveTo(
              svgTargetX * -1 + window.innerWidth / 2,
              svgTargetY * -1 + window.innerHeight / 2,
          );
          pz.zoomAbs(window.innerWidth / 2, window.innerHeight / 2, targetScale);
        }
      }

      panToAnchor.value = (anchor: SVGRectElement) => {
        panToElement(anchor, true);
      };


      // Pan to start element
      const start = processStart(svg);
      if (start) {
        console.info("[SVG] Found start element.");
        panToElement(start, false);

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

      // Anchors
      console.debug("[SVG] Processing anchors.");
      anchors.value = processAnchors(svg);
      console.info(`[SVG] Found ${anchors.value.length} anchors.`);

      // Links
      console.debug("[SVG] Processing hyperlinks.");
      const {anchor, hyper} = processHyperlinks(svg);
      console.info(`[SVG] Found ${anchor.length} anchor links and ${hyper.length} hyperlinks.`);
      anchor.forEach(([anchorId, element]) => {
        const anchor = anchors.value.find((a) => a.id == anchorId);
        if (!anchor) {
          console.error(`[SVG] Could not find anchor #${anchorId}!`);
          return;
        }
        element.addEventListener("click", () => {
          panToElement(anchor, true);
        });
      });

      // Audio areas
      console.debug("[SVG] Processing audio areas.");
      audioAreas.value = processAudio(svg);
      console.info(`[SVG] Found ${audioAreas.value.length} audio areas.`);

      // Videoscrolls
      console.debug("[SVG] Processing video scrolls.");
      scrolls.value = await processScrolls(svg);
      console.info(`[SVG] Found ${scrolls.value.length} video scrolls.`);

      // Debug Stats
      let stats: Stats | undefined;
      if (process.env.VUE_APP_DEMO) {
        console.info("[SVG] DEMO mode active, turning on stats & dev panel.");
        stats = new Stats();
        document.body.appendChild(stats.dom);
      } else {
        Array.from(document.body.getElementsByClassName("dev")).forEach((el) => {
          (el as HTMLElement).style.display = "none";
        });
      }

      // Animations: FPS Counter, Edge scrolling
      let mouse: MouseEvent | undefined;
      window.addEventListener("mousemove", (ev) => {
        mouse = ev;
        const transform = pz.getTransform();
        const currentRatio = svg.clientWidth * transform.scale / svg.viewBox.baseVal.width;
        mousePosition.x = (mouse.clientX - transform.x) / currentRatio;
        mousePosition.y = (mouse.clientY - transform.y) / currentRatio;
      });

      function animate() {
        if (stats) {
          stats.begin();
        }

        // Edge scrolling
        const MOVE_EDGE_X = window.innerWidth * .25;
        const MOVE_EDGE_Y = window.innerHeight * .25;
        const MAX_SPEED = 20;

        if (mouse && !panning.value && document.fullscreenElement) {
          let horizontalShift: number;
          let verticalShift: number;

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

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

          if (horizontalShift || verticalShift) {
            pz.moveTo(transform!.x + horizontalShift, transform!.y + verticalShift);
          }
        }

        if (stats) {
          stats.end();
        }

        requestAnimationFrame(animate);
      }

      requestAnimationFrame(animate);
    });


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

function processAnchors(document: XMLDocument): SVGRectElement[] {
  const result: SVGRectElement[] = [];
  Array.from(document.getElementsByTagName("rect"))
      .filter((el) => el.id.startsWith("anchor"))
      .forEach((anchor) => {
        console.debug(`[SVG/ANCHORS] Found anchor #${anchor.id}.`);
        anchor.classList.add("internal");
        result.push(anchor);
      });
  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 = Array.from(el.children).find((el) => el.tagName == "desc");
            console.debug(`[SVG/VIDEOSCROLLS] Found video scroll #${el.id}: ${descNode?.textContent}`);
            const [directionString, filesURL] = descNode!.textContent!.split("\n");

            const directions: VideoScrollDirection[] = directionString.split(" ").map((direction) => {
              if (!Object.values(VideoScrollDirection).includes(direction as VideoScrollDirection)) {
                throw new Error(`Unknown direction string: "${direction}"`);
              }
              return direction as VideoScrollDirection;
            });


            console.debug(`[SVG/VIDEOSCROLLS] Fetching ${filesURL}...`);
            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}`);

            let x = el.x.baseVal.value;
            let y = el.y.baseVal.value;
            let w = el.width.baseVal.value;
            let h = el.height.baseVal.value;
            let angle = 0;

            const transform = el.attributes.getNamedItem("transform");
            const rotateResult = /rotate\((-?[0-9.]+)\)/.exec(transform?.value || "");
            if (rotateResult) {
              angle = parseFloat(rotateResult[1]);
              const [ncx, ncy] = rotate(x + w / 2, y + h / 2, 0, 0, angle);
              x = ncx - w / 2;
              y = ncy - h / 2;
            }

            return {
              top: y * ratio,
              left: x * ratio,
              angle,
              width: w * ratio,
              height: h * ratio,
              directions,
              files
            };
          })
  );
}

function processAudio(svg: XMLDocument): AudioAreaDef[] {
  return Array.from(svg.getElementsByTagName("circle"))
      .filter((el) => Array.from(el.children).some((el) => el.tagName == "desc"))
      .map((el) => {
        const descNode = Array.from(el.children).find((el) => el.tagName == "desc");
        console.debug(`[SVG/AUDIOAREAS] Found audio area #${el.id}: ${descNode?.textContent}`);
        const audioSrc = descNode!.textContent!.trim();

        el.classList.add("internal");

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

function processHyperlinks(svg: XMLDocument): { anchor: [string, SVGAElement][], hyper: SVGAElement[] } {
  const anchor: [string, SVGAElement][] = [];
  const hyper: SVGAElement[] = [];
  Array.from(svg.getElementsByTagName("a")).forEach((el) => {
    if (el.getAttribute("xlink:href")?.startsWith("anchor")) {
      anchor.push([el.getAttribute("xlink:href") as string, el as unknown as SVGAElement]);
      el.setAttribute("xlink:href", "#");
    } else {
      el.setAttribute("target", "_blank");
      hyper.push(el as unknown as SVGAElement);
    }
  });
  return {anchor, hyper};
}

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;
}

.devpanel div {
  display: flex;
  justify-content: space-between;
}

.devpanel label {
  float: right;
}

.devpanel div span {
  margin: 0 .5em;
}
</style>
