import axios from "axios";
import * as R from "ramda";
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
import * as TWEEN from "@tweenjs/tween.js";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";

const ANIMATION_DURATION_SECS = 10;
const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/";

export async function loadLandmarks(vue, world, scene, loop) {
  vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);

  try {
    await db.open();
    const settings = await db.settings.get(MASTER_SETTINGS_KEY);
    const activeDid = settings?.activeDid || "";
    const apiServer = settings?.apiServer;
    await accountsDB.open();
    const accounts = await accountsDB.accounts.toArray();
    const account = R.find((acc) => acc.did === activeDid, accounts);
    const headers = {
      "Content-Type": "application/json",
    };
    const identity = JSON.parse(account?.identity || "null");
    if (identity) {
      const token = await accessToken(identity);
      headers["Authorization"] = "Bearer " + token;
    }

    const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
    const resp = await axios.get(url, { headers: headers });
    if (resp.status === 200) {
      const landmarks = resp.data.data;

      const minDate = landmarks[landmarks.length - 1].issuedAt;
      const maxDate = landmarks[0].issuedAt;

      world.setExposedWorldProperties("startTime", minDate.replace("T", " "));
      world.setExposedWorldProperties("endTime", maxDate.replace("T", " "));

      const minTimeMillis = new Date(minDate).getTime();
      const fullTimeMillis =
        maxDate > minDate ? new Date(maxDate).getTime() - minTimeMillis : 1; // avoid divide by zero
      // ratio of animation time to real time
      const fakeRealRatio = (ANIMATION_DURATION_SECS * 1000) / fullTimeMillis;

      // load plant model first because it takes a second
      const loader = new GLTFLoader();
      // choose the right plant
      const modelLoc = "/models/lupine_plant/scene.gltf", // push with pokies
        modScale = 0.1;
      //const modelLoc = "/models/round_bush/scene.gltf", // green & pink
      //  modScale = 1;
      //const modelLoc = "/models/coreopsis-flower.glb", // 3 flowers
      //  modScale = 2;
      //const modelLoc = "/models/a_bush/scene.gltf", // purple leaves
      //  modScale = 15;

      // calculate positions for each claim, especially because some are random
      const locations = landmarks.map((claim) =>
        locForGive(
          claim,
          world.PLATFORM_SIZE,
          world.PLATFORM_EDGE_FOR_UNKNOWNS,
        ),
      );

      // eslint-disable-next-line @typescript-eslint/no-this-alias
      loader.load(
        modelLoc,
        function (gltf) {
          gltf.scene.scale.set(0, 0, 0);
          for (let i = 0; i < landmarks.length; i++) {
            // claim is a GiveServerRecord (see endorserServer.ts)
            const claim = landmarks[i];
            const newPlant = SkeletonUtils.clone(gltf.scene);

            const loc = locations[i];
            newPlant.position.set(loc.x, 0, loc.z);

            world.scene.add(newPlant);
            const timeDelayMillis =
              fakeRealRatio *
              (new Date(claim.issuedAt).getTime() - minTimeMillis);
            new TWEEN.Tween(newPlant.scale)
              .delay(timeDelayMillis)
              .to({ x: modScale, y: modScale, z: modScale }, 5000)
              .start();
            world.bushes = [...world.bushes, newPlant];
          }
        },
        undefined,
        function (error) {
          console.error(error);
        },
      );

      // calculate when lights shine on appearing claim area
      for (let i = 0; i < landmarks.length; i++) {
        // claim is a GiveServerRecord (see endorserServer.ts)
        const claim = landmarks[i];

        const loc = locations[i];
        const light = createLight();
        light.position.set(loc.x, 20, loc.z);
        light.target.position.set(loc.x, 0, loc.z);
        loop.updatables.push(light);
        scene.add(light);
        scene.add(light.target);

        // now figure out the timing and shine a light
        const timeDelayMillis =
          fakeRealRatio * (new Date(claim.issuedAt).getTime() - minTimeMillis);
        new TWEEN.Tween(light)
          .delay(timeDelayMillis)
          .to({ intensity: 100 }, 10)
          .chain(
            new TWEEN.Tween(light.position)
              .to({ y: 5 }, 5000)
              .onComplete(() => {
                scene.remove(light);
                light.dispose();
              }),
          )
          .start();
        world.lights = [...world.lights, light];
      }
    } else {
      console.error(
        "Got bad server response status & data of",
        resp.status,
        resp.data,
      );
      vue.setAlert(
        "Error With Server",
        "There was an error retrieving your claims from the server.",
      );
    }
  } catch (error) {
    console.error("Got exception contacting server:", error);
    vue.setAlert(
      "Error With Server",
      "There was a problem retrieving your claims from the server.",
    );
  }
}

/**
 *
 * @param giveClaim
 * @returns {x:float, z:float} where -50 <= x & z < 50
 */
function locForGive(giveClaim, platformWidth, borderWidth) {
  let loc;
  if (giveClaim?.claim?.recipient?.identifier) {
    // this is directly to a person
    loc = locForEthrDid(giveClaim.claim.recipient.identifier);
    loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 };
  } else if (giveClaim?.object?.isPartOf?.identifier) {
    // this is probably to a project
    const objId = giveClaim.object.isPartOf.identifier;
    if (objId.startsWith(ENDORSER_ENTITY_PREFIX)) {
      loc = locForUlid(objId.substring(ENDORSER_ENTITY_PREFIX.length));
      loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 };
    }
  }
  if (!loc) {
    // it must be outside our known addresses so let's put it somewhere random on the side
    const leftSide = Math.random() < 0.5;
    loc = {
      x: leftSide
        ? -platformWidth / 2 - borderWidth / 2
        : platformWidth / 2 + borderWidth / 2,
      z: Math.random() * platformWidth - platformWidth / 2,
    };
  }
  return loc;
}

/**
 * Generate a deterministic x & z location based on the randomness of an ID.
 *
 * We'd like the location to fully map back to the original ID.
 * This typically means we use half the ID for the x and half for the z.
 *
 * ... in this case: a ULID.
 * We'll use the first half (13 characters) for the x coordinate and next 13 for the z.
 * We recognize that this is only 3 characters = 15 bits = 32768 unique values
 * for the random part for the first half. We also recognize that those random
 * bits may be shared with previous ULIDs if they were generated in the same
 * millisecond, and therefore much of the evenness of the distribution depends
 * on the other dimension.
 *
 * Also: since the first 10 characters are time-based, we're going to reverse
 * the order of the characters to make the randomness more evenly distributed.
 * This is reversing the order of the 5-bit characters, not each of the bits.
 * Also wik: the first characters of the second half might be the same as
 * previous ULIDs if they were generated in the same millisecond. So it's
 * best to have that last character be the most significant bit so that there
 * is a more even distribution in that dimension.
 *
 * @param ulid
 * @returns {x: float, z: float} where 0 <= x & z < 100
 */
function locForUlid(ulid) {
  const xChars = ulid.substring(0, 13).split("").reverse().join("");
  const zChars = ulid.substring(13, 26).split("").reverse().join("");

  // from https://github.com/ulid/javascript/blob/5e9727b527aec5b841737c395a20085c4361e971/lib/index.ts#L21
  const BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford's Base32

  // We're currently only using 1024 possible x and z values
  // because the display is pretty low-fidelity at this point.
  const rawX = BASE32.indexOf(xChars[1]) * 32 + BASE32.indexOf(xChars[0]);
  const rawZ = BASE32.indexOf(zChars[1]) * 32 + BASE32.indexOf(zChars[0]);

  const x = (100 * rawX) / 1024;
  const z = (100 * rawZ) / 1024;
  return { x, z };
}

/**
 * See locForUlid. Similar, but for ethr DIDs.
 * @param did
 * @returns {x: float, z: float} where 0 <= x & z < 100
 */
function locForEthrDid(did) {
  // "did:ethr:0x..."
  if (did.length < 51) {
    return { x: 0, z: 0 };
  } else {
    const randomness = did.substring("did:ethr:0x".length);
    // We'll use all the randomness for fully unique x & z values.
    // But we'll only calculate this view with the first byte since our rendering resolution is low.
    const xOff = parseInt(Number("0x" + randomness.substring(0, 2)), 10);
    const x = (xOff * 100) / 256;
    // ... and since we're reserving 20 bytes total for x, start z with character 20,
    // again with one byte.
    const zOff = parseInt(Number("0x" + randomness.substring(20, 22)), 10);
    const z = (zOff * 100) / 256;
    return { x, z };
  }
}

function createLight() {
  const light = new THREE.SpotLight(0xffffff, 0, 0, Math.PI / 8, 0.5, 0);
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  light.tick = () => {};
  return light;
}
