import * as Cesium from "cesium";

function BiasOptions(options) {
  this.type = options.type;
  this.polygonOffset = Cesium.knockout.observable(options.polygonOffset);
  this.polygonOffsetFactor = Cesium.knockout.observable(
    options.polygonOffsetFactor,
  );
  this.polygonOffsetUnits = Cesium.knockout.observable(
    options.polygonOffsetUnits,
  );
  this.normalOffset = Cesium.knockout.observable(options.normalOffset);
  this.normalOffsetScale = Cesium.knockout.observable(
    options.normalOffsetScale,
  );
  this.normalShading = Cesium.knockout.observable(options.normalShading);
  this.normalShadingSmooth = Cesium.knockout.observable(
    options.normalShadingSmooth,
  );
  this.depthBias = Cesium.knockout.observable(options.depthBias);
}

const viewModel = {
  lightAngle: 40.0,
  lightAngleEnabled: true,
  lightHorizon: 70.0,
  lightHorizonEnabled: true,
  distance: 10000.0,
  distanceEnabled: true,
  radius: 200.0,
  radiusEnabled: true,
  darkness: 0.3,
  shadows: true,
  terrain: true,
  globe: true,
  terrainCast: true,
  terrainReceive: true,
  debug: true,
  freeze: false,
  cascadeColors: false,
  cascadeColorsEnabled: true,
  fitNearFar: true,
  fitNearFarEnabled: true,
  softShadows: false,
  softShadowsEnabled: true,
  cascadeOptions: [1, 4],
  cascades: 4,
  cascadesEnabled: true,
  lightSourceOptions: ["Freeform", "Sun", "Fixed", "Point", "Spot"],
  lightSource: "Freeform",
  sizeOptions: [256, 512, 1024, 2048],
  size: 1024,
  modelOptions: [
    "Wood Tower",
    "Cesium Air",
    "Cesium Man",
    "Transparent Box",
    "Shadow Tester",
    "Shadow Tester 2",
    "Shadow Tester 3",
    "Shadow Tester 4",
    "Shadow Tester Point",
  ],
  model: "Shadow Tester",
  locationOptions: [
    "Exton",
    "Everest",
    "Pinnacle PA",
    "Seneca Rocks",
    "Half Dome",
    "3D Tiles",
  ],
  location: "Pinnacle PA",
  modelPositionOptions: ["Center", "Ground", "High", "Higher", "Space"],
  modelPosition: "Center",
  grid: false,
  biasModes: [
    new BiasOptions({
      type: "terrain",
      polygonOffset: true,
      polygonOffsetFactor: 1.1,
      polygonOffsetUnits: 4.0,
      normalOffset: true,
      normalOffsetScale: 0.5,
      normalShading: true,
      normalShadingSmooth: 0.3,
      depthBias: 0.0001,
    }),
    new BiasOptions({
      type: "primitive",
      polygonOffset: true,
      polygonOffsetFactor: 1.1,
      polygonOffsetUnits: 4.0,
      normalOffset: true,
      normalOffsetScale: 0.1,
      normalShading: true,
      normalShadingSmooth: 0.05,
      depthBias: 0.00001,
    }),
    new BiasOptions({
      type: "point",
      polygonOffset: false,
      polygonOffsetFactor: 1.1,
      polygonOffsetUnits: 4.0,
      normalOffset: false,
      normalOffsetScale: 0.0,
      normalShading: true,
      normalShadingSmooth: 0.1,
      depthBias: 0.0005,
    }),
  ],
  biasMode: Cesium.knockout.observable(),
};

const uiOptions = {
  all: [
    "lightHorizon",
    "lightAngle",
    "distance",
    "radius",
    "terrainCast",
    "cascades",
    "cascadeColors",
    "fitNearFar",
    "softShadows",
  ],
  disable: {
    Freeform: ["radius"],
    Sun: ["lightHorizon", "lightAngle", "radius"],
    Fixed: [
      "lightHorizon",
      "lightAngle",
      "distance",
      "radius",
      "cascades",
      "cascadeColors",
      "fitNearFar",
    ],
    Point: [
      "lightHorizon",
      "lightAngle",
      "distance",
      "cascades",
      "cascadeColors",
      "fitNearFar",
      "softShadows",
    ],
    Spot: [
      "lightHorizon",
      "lightAngle",
      "distance",
      "radius",
      "cascades",
      "cascadeColors",
      "fitNearFar",
    ],
  },
  modelUrls: {
    "Wood Tower": "../../SampleData/models/WoodTower/Wood_Tower.glb",
    "Cesium Air": "../../SampleData/models/CesiumAir/Cesium_Air.glb",
    "Cesium Man": "../../SampleData/models/CesiumMan/Cesium_Man.glb",
    "Transparent Box":
      "../../SampleData/models/ShadowTester/Shadow_Transparent.glb",
    "Shadow Tester": "../../SampleData/models/ShadowTester/Shadow_Tester.glb",
    "Shadow Tester 2":
      "../../SampleData/models/ShadowTester/Shadow_Tester_2.glb",
    "Shadow Tester 3":
      "../../SampleData/models/ShadowTester/Shadow_Tester_3.glb",
    "Shadow Tester 4":
      "../../SampleData/models/ShadowTester/Shadow_Tester_4.glb",
    "Shadow Tester Point":
      "../../SampleData/models/ShadowTester/Shadow_Tester_Point.glb",
  },
  locations: {
    Exton: {
      centerLongitude: -1.31968,
      centerLatitude: 0.698874,
    },
    Everest: {
      centerLongitude: 1.517132688,
      centerLatitude: 0.4884844964,
    },
    "Pinnacle PA": {
      centerLongitude: -1.3324415110874286,
      centerLatitude: 0.6954224325279967,
    },
    "Seneca Rocks": {
      centerLongitude: -1.38519677,
      centerLatitude: 0.67781497,
    },
    "Half Dome": {
      centerLongitude: -2.0862479628,
      centerLatitude: 0.6587902522,
    },
    "3D Tiles": {
      centerLongitude: -1.31968,
      centerLatitude: 0.698874,
      tileset: "../../SampleData/Cesium3DTiles/Tilesets/Tileset/tileset.json",
    },
  },
};

Cesium.knockout.track(viewModel);
const toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);
Cesium.knockout
  .getObservable(viewModel, "lightAngle")
  .subscribe(updateLightDirection);
Cesium.knockout
  .getObservable(viewModel, "lightHorizon")
  .subscribe(updateLightDirection);
Cesium.knockout.getObservable(viewModel, "distance").subscribe(updateSettings);
Cesium.knockout.getObservable(viewModel, "radius").subscribe(updateSettings);
Cesium.knockout.getObservable(viewModel, "darkness").subscribe(updateSettings);
Cesium.knockout.getObservable(viewModel, "debug").subscribe(updateSettings);
Cesium.knockout.getObservable(viewModel, "freeze").subscribe(updateSettings);
Cesium.knockout.getObservable(viewModel, "shadows").subscribe(updateSettings);
Cesium.knockout.getObservable(viewModel, "terrain").subscribe(updateLocation);
Cesium.knockout.getObservable(viewModel, "globe").subscribe(updateSettings);
Cesium.knockout
  .getObservable(viewModel, "terrainCast")
  .subscribe(updateSettings);
Cesium.knockout
  .getObservable(viewModel, "terrainReceive")
  .subscribe(updateSettings);
Cesium.knockout
  .getObservable(viewModel, "fitNearFar")
  .subscribe(updateSettings);
Cesium.knockout
  .getObservable(viewModel, "cascadeColors")
  .subscribe(updateSettings);
Cesium.knockout
  .getObservable(viewModel, "softShadows")
  .subscribe(updateSettings);
Cesium.knockout.getObservable(viewModel, "cascades").subscribe(updateShadows);
Cesium.knockout
  .getObservable(viewModel, "lightSource")
  .subscribe(updateShadows);
Cesium.knockout.getObservable(viewModel, "size").subscribe(updateSettings);
Cesium.knockout.getObservable(viewModel, "model").subscribe(updateModels);
Cesium.knockout
  .getObservable(viewModel, "modelPosition")
  .subscribe(updateLocation);
Cesium.knockout.getObservable(viewModel, "grid").subscribe(updateModels);
Cesium.knockout.getObservable(viewModel, "location").subscribe(updateLocation);

for (let i = 0; i < viewModel.biasModes.length; ++i) {
  const biasMode = viewModel.biasModes[i];
  biasMode.polygonOffset.subscribe(updateSettings);
  biasMode.polygonOffsetFactor.subscribe(updateSettings);
  biasMode.polygonOffsetUnits.subscribe(updateSettings);
  biasMode.normalOffset.subscribe(updateSettings);
  biasMode.normalOffsetScale.subscribe(updateSettings);
  biasMode.normalShading.subscribe(updateSettings);
  biasMode.normalShadingSmooth.subscribe(updateSettings);
  biasMode.depthBias.subscribe(updateSettings);
}

const viewer = new Cesium.Viewer("cesiumContainer", {
  scene3DOnly: true,
  infoBox: false,
  selectionIndicator: false,
  timeline: false,
});

const offset = new Cesium.Cartesian3();
const scene = viewer.scene;
const freeformLightCamera = new Cesium.Camera(scene);

function updateLightDirection() {
  const location = uiOptions.locations[viewModel.location];
  const center = Cesium.Cartesian3.fromRadians(
    location.centerLongitude,
    location.centerLatitude,
    location.height,
  );
  const lightHorizon = Cesium.Math.toRadians(viewModel.lightHorizon);
  const lightAngle = Cesium.Math.toRadians(viewModel.lightAngle);
  offset.z = Math.cos(lightHorizon);
  offset.x = Math.sin(lightAngle) * (1.0 - offset.z);
  offset.y = Math.cos(lightAngle) * (1.0 - offset.z);

  freeformLightCamera.lookAt(center, offset);
}

const context = scene.context;
const camera = scene.camera;
const globe = scene.globe;
let shadowMap;

function updateSettings() {
  shadowMap.maximumDistance = Number(viewModel.distance);
  shadowMap._pointLightRadius = Number(viewModel.radius);
  shadowMap._fitNearFar = viewModel.fitNearFar;
  shadowMap.darkness = viewModel.darkness;
  shadowMap.debugShow = viewModel.debug;
  shadowMap.debugFreezeFrame = viewModel.freeze;
  shadowMap.enabled = viewModel.shadows;
  shadowMap.size = viewModel.size;
  shadowMap.debugCascadeColors = viewModel.cascadeColors;
  shadowMap.softShadows = viewModel.softShadows;

  // Update biases
  for (let i = 0; i < viewModel.biasModes.length; ++i) {
    const biasMode = viewModel.biasModes[i];
    const bias = shadowMap[`_${biasMode.type}Bias`];
    bias.polygonOffset =
      !shadowMap._isPointLight &&
      shadowMap._polygonOffsetSupported &&
      biasMode.polygonOffset();
    bias.polygonOffsetFactor = biasMode.polygonOffsetFactor();
    bias.polygonOffsetUnits = biasMode.polygonOffsetUnits();
    bias.normalOffset = biasMode.normalOffset();
    bias.normalOffsetScale = biasMode.normalOffsetScale();
    bias.normalShading = biasMode.normalShading();
    bias.normalShadingSmooth = biasMode.normalShadingSmooth();
    bias.depthBias = biasMode.depthBias();
  }

  // Update render states for when polygon offset values change
  shadowMap.debugCreateRenderStates();

  // Force all derived commands to update
  shadowMap.dirty = true;

  globe.shadows = Cesium.ShadowMode.fromCastReceive(
    viewModel.terrainCast,
    viewModel.terrainReceive,
  );
  globe.show = viewModel.globe;
  scene.skyAtmosphere.show = viewModel.globe;
}

const sunCamera = scene._sunCamera;
const fixedLightCamera = new Cesium.Camera(scene);
const pointLightCamera = new Cesium.Camera(scene);
const spotLightCamera = new Cesium.Camera(scene);

function updateShadows() {
  const cascades = viewModel.cascades;
  const lightSource = viewModel.lightSource;

  let lightCamera;
  if (lightSource === "Freeform") {
    lightCamera = freeformLightCamera;
  } else if (lightSource === "Sun") {
    lightCamera = sunCamera;
  }

  let shadowOptions;

  if (lightSource === "Fixed") {
    shadowOptions = {
      context: context,
      lightCamera: fixedLightCamera,
      cascadesEnabled: false,
    };
  } else if (lightSource === "Point") {
    shadowOptions = {
      context: context,
      lightCamera: pointLightCamera,
      isPointLight: true,
    };
  } else if (lightSource === "Spot") {
    shadowOptions = {
      context: context,
      lightCamera: spotLightCamera,
      cascadesEnabled: false,
    };
  } else if (cascades === 4) {
    shadowOptions = {
      context: context,
      lightCamera: lightCamera,
    };
  } else if (cascades === 1) {
    shadowOptions = {
      context: context,
      lightCamera: lightCamera,
      numberOfCascades: 1,
    };
  }

  scene.shadowMap.destroy();
  scene.shadowMap = new Cesium.ShadowMap(shadowOptions);

  shadowMap = scene.shadowMap;
  shadowMap.enabled = true;
  shadowMap.debugShow = true;

  updateSettings();
  updateUI();
}

function updateUI() {
  uiOptions.all.forEach(function (setting) {
    if (uiOptions.disable[viewModel.lightSource].indexOf(setting) > -1) {
      viewModel[`${setting}Enabled`] = false;
    } else {
      viewModel[`${setting}Enabled`] = true;
    }
  });
}

scene.debugShowFramesPerSecond = true;

function getModelPosition() {
  if (viewModel.modelPosition === "Ground") {
    return 0.0;
  } else if (viewModel.modelPosition === "Center") {
    return 50.0;
  } else if (viewModel.modelPosition === "High") {
    return 5000.0;
  } else if (viewModel.modelPosition === "Higher") {
    return 20000.0;
  } else if (viewModel.modelPosition === "Space") {
    return 10000000.0;
  }
}

const ellipsoidTerrainProvider = new Cesium.EllipsoidTerrainProvider();
let worldTerrain;
try {
  worldTerrain = await Cesium.createWorldTerrainAsync();
} catch (error) {
  console.log(`Failed to create world terrain. ${error}`);
}

async function updateLocation() {
  // Get the height of the terrain at the given longitude/latitude, then create the scene.
  const location = uiOptions.locations[viewModel.location];
  const positions = [
    new Cesium.Cartographic(location.centerLongitude, location.centerLatitude),
  ];

  let terrainProvider = ellipsoidTerrainProvider;
  if (viewModel.terrain && Cesium.defined(worldTerrain)) {
    terrainProvider = worldTerrain;
  }

  scene.terrainProvider = terrainProvider;

  try {
    const updatedPositions = await Cesium.sampleTerrain(
      terrainProvider,
      11,
      positions,
    );
    location.height = updatedPositions[0].height + getModelPosition();
  } catch (error) {
    console.log(`There was an error sampling terrain. ${error}`);
  }

  createScene();
}

updateLocation();

function createScene() {
  const location = uiOptions.locations[viewModel.location];
  const center = Cesium.Cartesian3.fromRadians(
    location.centerLongitude,
    location.centerLatitude,
    location.height,
  );

  const frustumSize = 55.0;
  const frustumNear = 1.0;
  const frustumFar = 400.0;
  const frustum = new Cesium.OrthographicOffCenterFrustum();
  frustum.left = -frustumSize;
  frustum.right = frustumSize;
  frustum.bottom = -frustumSize;
  frustum.top = frustumSize;
  frustum.near = frustumNear;
  frustum.far = frustumFar;

  fixedLightCamera.frustum = frustum;
  fixedLightCamera.lookAt(center, new Cesium.Cartesian3(30.0, 30.0, 50.0));

  spotLightCamera.frustum.fov = Cesium.Math.PI_OVER_TWO;
  spotLightCamera.frustum.aspectRatio = 1.0;
  spotLightCamera.frustum.near = 1.0;
  spotLightCamera.frustum.far = 500.0;
  spotLightCamera.lookAt(center, new Cesium.Cartesian3(30.0, 30.0, 50.0));

  pointLightCamera.position = center;

  camera.lookAt(center, new Cesium.Cartesian3(25.0, 25.0, 30.0));

  updateLightDirection();
  updateModels();
  updateShadows();
}

function updateModels() {
  scene.primitives.removeAll();

  const location = uiOptions.locations[viewModel.location];
  const centerLongitude = location.centerLongitude;
  const centerLatitude = location.centerLatitude;
  const height = location.height;

  const position1 = Cesium.Cartesian3.fromRadians(
    centerLongitude,
    centerLatitude,
    height + 5.0,
  );
  const position2 = Cesium.Cartesian3.fromRadians(
    centerLongitude,
    centerLatitude,
    height + 10.0,
  );
  const position3 = Cesium.Cartesian3.fromRadians(
    centerLongitude,
    centerLatitude,
    height + 15.0,
  );
  const modelPosition = Cesium.Cartesian3.fromRadians(
    centerLongitude,
    centerLatitude,
    height,
  );

  createModel(uiOptions.modelUrls[viewModel.model], modelPosition);
  createBox(position3);
  createBoxRTC(position2);
  createSphere(position1);

  if (Cesium.defined(location.tileset)) {
    createTileset(location.tileset);
  }

  // Add a grid of models
  if (viewModel.grid) {
    const spacing = 0.00002;
    const gridSize = 10;
    for (let i = 0; i < gridSize * gridSize; ++i) {
      const x = i % gridSize;
      const y = Math.floor(i / gridSize);
      const longitude = centerLongitude + spacing * (x - gridSize / 2.0);
      const latitude = centerLatitude + spacing * (y - gridSize / 2.0);
      const position = Cesium.Cartesian3.fromRadians(
        longitude,
        latitude,
        height,
      );
      createModel(uiOptions.modelUrls[viewModel.model], position);
    }
  }
}

async function createTileset(url) {
  try {
    const tileset = await Cesium.Cesium3DTileset.fromUrl(url);
    const location = uiOptions.locations[viewModel.location];
    if (location.tileset !== url) {
      // Another option was loaded. Discard the result;
      return;
    }
    viewer.scene.primitives.add(tileset);
  } catch (error) {
    console.log(`Error loading tileset: ${error}`);
  }
}

async function createModel(url, origin) {
  const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
    origin,
    new Cesium.HeadingPitchRoll(),
  );

  try {
    const model = scene.primitives.add(
      await Cesium.Model.fromGltfAsync({
        url: url,
        modelMatrix: modelMatrix,
      }),
    );

    model.readyEvent.addEventListener(() => {
      // Play and loop all animations at half-speed
      model.activeAnimations.addAll({
        multiplier: 0.5,
        loop: Cesium.ModelAnimationLoop.REPEAT,
      });
    });

    return model;
  } catch (error) {
    window.alert(error);
  }
}

function createBoxRTC(origin) {
  const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
    origin,
    new Cesium.HeadingPitchRoll(),
  );

  const boxGeometry = Cesium.BoxGeometry.createGeometry(
    Cesium.BoxGeometry.fromDimensions({
      vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
      dimensions: new Cesium.Cartesian3(1.0, 1.0, 1.0),
    }),
  );

  const positions = boxGeometry.attributes.position.values;
  const newPositions = new Float32Array(positions.length);
  for (let i = 0; i < positions.length; ++i) {
    newPositions[i] = positions[i];
  }
  boxGeometry.attributes.position.values = newPositions;
  boxGeometry.attributes.position.componentDatatype =
    Cesium.ComponentDatatype.FLOAT;

  Cesium.BoundingSphere.transform(
    boxGeometry.boundingSphere,
    modelMatrix,
    boxGeometry.boundingSphere,
  );

  const boxGeometryInstance = new Cesium.GeometryInstance({
    geometry: boxGeometry,
    attributes: {
      color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.BLUE),
    },
  });

  const box = new Cesium.Primitive({
    geometryInstances: boxGeometryInstance,
    appearance: new Cesium.PerInstanceColorAppearance({
      translucent: false,
      closed: true,
    }),
    asynchronous: false,
    rtcCenter: boxGeometry.boundingSphere.center,
    shadows: Cesium.ShadowMode.ENABLED,
  });

  scene.primitives.add(box);
}

function createBox(origin) {
  const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
    origin,
    new Cesium.HeadingPitchRoll(),
  );

  const box = new Cesium.Primitive({
    geometryInstances: new Cesium.GeometryInstance({
      geometry: Cesium.BoxGeometry.fromDimensions({
        dimensions: new Cesium.Cartesian3(0.5, 0.5, 0.5),
        vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
      }),
      modelMatrix: modelMatrix,
      attributes: {
        color: Cesium.ColorGeometryInstanceAttribute.fromColor(
          Cesium.Color.BLUE,
        ),
      },
    }),
    appearance: new Cesium.PerInstanceColorAppearance({
      translucent: false,
      closed: true,
    }),
    asynchronous: false,
    shadows: Cesium.ShadowMode.ENABLED,
  });

  scene.primitives.add(box);
}

function createSphere(origin) {
  const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
    origin,
    new Cesium.HeadingPitchRoll(),
  );

  const sphere = new Cesium.Primitive({
    geometryInstances: new Cesium.GeometryInstance({
      geometry: new Cesium.SphereGeometry({
        radius: 2.0,
        vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
      }),
      modelMatrix: modelMatrix,
      attributes: {
        color: Cesium.ColorGeometryInstanceAttribute.fromColor(
          new Cesium.Color(1.0, 0.0, 0.0, 0.5),
        ),
      },
    }),
    appearance: new Cesium.PerInstanceColorAppearance({
      translucent: true,
      closed: true,
    }),
    asynchronous: false,
    shadows: Cesium.ShadowMode.ENABLED,
  });

  scene.primitives.add(sphere);
}

const canvas = viewer.canvas;
canvas.setAttribute("tabindex", "0"); // needed to put focus on the canvas
canvas.onclick = function () {
  // To get key events
  canvas.focus();
};

const handler = new Cesium.ScreenSpaceEventHandler(canvas);

// Click object to turn castShadows on/off
handler.setInputAction(function (movement) {
  const picked = scene.pick(movement.position);
  if (Cesium.defined(picked) && Cesium.defined(picked.primitive)) {
    const castShadows = Cesium.ShadowMode.castShadows(picked.primitive.shadows);
    const receiveShadows = Cesium.ShadowMode.receiveShadows(
      picked.primitive.shadows,
    );
    picked.primitive.shadows = Cesium.ShadowMode.fromCastReceive(
      !castShadows,
      receiveShadows,
    );
  }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

// Middle click object to turn receiveShadows on/off
handler.setInputAction(function (movement) {
  const picked = scene.pick(movement.position);
  if (Cesium.defined(picked)) {
    const castShadows = Cesium.ShadowMode.castShadows(picked.primitive.shadows);
    const receiveShadows = Cesium.ShadowMode.receiveShadows(
      picked.primitive.shadows,
    );
    picked.primitive.shadows = Cesium.ShadowMode.fromCastReceive(
      castShadows,
      !receiveShadows,
    );
  }
}, Cesium.ScreenSpaceEventType.MIDDLE_CLICK);
