import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";

const viewer = new Cesium.Viewer("cesiumContainer", {
  orderIndependentTranslucency: false,
});

viewer.clock.currentTime = Cesium.JulianDate.fromIso8601(
  "2021-11-09T20:27:37.016064475348684937Z",
);

// Model positioning ===============================================

const position = Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706, 0);
const hpr = new Cesium.HeadingPitchRoll(0, 0, 0);
const fixedFrameTransform = Cesium.Transforms.localFrameToFixedFrameGenerator(
  "north",
  "west",
);

// Custom Shader Definitions ========================================

// Dragging the mouse will expand/shrink the model.
const expandModelShader = new Cesium.CustomShader({
  uniforms: {
    // Vector from latest drag center to the mouse
    u_drag: {
      type: Cesium.UniformType.VEC2,
      value: new Cesium.Cartesian2(0.0, 0.0),
    },
  },
  vertexShaderText: `
            // If the mouse is dragged to the right, the model grows
            // If the mouse is dragged to the left, the model shrinks
            void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput)
            {
                vsOutput.positionMC += 0.01 * u_drag.x * vsInput.attributes.normalMC;
            }
            `,
});

const textureUniformShader = new Cesium.CustomShader({
  uniforms: {
    // elapsed time in seconds for animation
    u_time: {
      type: Cesium.UniformType.FLOAT,
      value: 0,
    },
    // user-defined texture
    u_stripes: {
      type: Cesium.UniformType.SAMPLER_2D,
      value: new Cesium.TextureUniform({
        url: "../../SampleData/cesium_stripes.png",
      }),
    },
  },
  // Apply the texture to the model, but move the texture coordinates
  // a bit over time so it's animated.
  fragmentShaderText: `
            void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
            {
                vec2 texCoord = fsInput.attributes.texCoord_0 + 0.1 * vec2(u_time, 0.0);
                material.diffuse = texture(u_stripes, texCoord).rgb;
            }
            `,
});

// make a checkerboard texture with an alpha that increases with the
// diagonal number
function makeCheckerboardTexture(textureSize) {
  const checkerboard = new Uint8Array(4 * textureSize * textureSize);

  const maxDiagonal = 2 * (textureSize - 1);
  for (let i = 0; i < textureSize; i++) {
    for (let j = 0; j < textureSize; j++) {
      const index = i * textureSize + j;
      // Checking the parity of the diagonal number gives a checkerboard
      // pattern.
      const diagonal = i + j;
      if (diagonal % 2 === 0) {
        // set the square red. We only need to set the red channel!
        checkerboard[4 * index] = 255;
      }
      // otherwise we'd set the square to black. But arrays are already
      // initialized to 0s so nothing needed here.

      // for the alpha channel, map the diagonal number to [0, 255]
      checkerboard[4 * index + 3] = (255 * diagonal) / maxDiagonal;
    }
  }
  return new Cesium.TextureUniform({
    typedArray: checkerboard,
    width: textureSize,
    height: textureSize,
    // Don't interpolate, we want crisp checkerboard edges
    minificationFilter: Cesium.TextureMinificationFilter.NEAREST,
    magnificationFilter: Cesium.TextureMagnificationFilter.NEAREST,
  });
}
const checkerboardTexture = makeCheckerboardTexture(8);

// Use the checkerboard red channel as a mask
const checkerboardMaskShader = new Cesium.CustomShader({
  uniforms: {
    u_checkerboard: {
      type: Cesium.UniformType.SAMPLER_2D,
      value: checkerboardTexture,
    },
  },
  fragmentShaderText: `
            void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
            {
                vec2 texCoord = fsInput.attributes.texCoord_0;
                vec4 checkerboard = texture(u_checkerboard, texCoord);
                material.diffuse = mix(material.diffuse, vec3(0.0), checkerboard.r);
            }
            `,
});

// Color like a checkerboard but make the transparency vary with
// the diagonal
const checkerboardAlphaShader = new Cesium.CustomShader({
  uniforms: {
    u_checkerboard: {
      type: Cesium.UniformType.SAMPLER_2D,
      value: checkerboardTexture,
    },
  },
  // This model normally renders opaque so material.alpha would be ignored.
  // this setting forces the shader to render in the translucent pass.
  translucencyMode: Cesium.CustomShaderTranslucencyMode.TRANSLUCENT,
  fragmentShaderText: `
            void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
            {
                vec2 texCoord = fsInput.attributes.texCoord_0;
                vec4 checkerboard = texture(u_checkerboard, texCoord);
                material.diffuse = checkerboard.rgb;
                material.alpha = checkerboard.a;
            }
            `,
});

// Use the checkerboard to cut holes in the model
const checkerboardHolesShader = new Cesium.CustomShader({
  uniforms: {
    u_checkerboard: {
      type: Cesium.UniformType.SAMPLER_2D,
      value: checkerboardTexture,
    },
  },
  fragmentShaderText: `
            void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
            {
                vec2 texCoord = fsInput.attributes.texCoord_0;
                vec4 checkerboard = texture(u_checkerboard, texCoord);
                if (checkerboard.r > 0.0) {
                    discard;
                }
            }
            `,
});

// This example is to demonstrate the conventions used for orienting
// the texture. +x is to the right and +y is from **bottom to top**.
// This is to be consistent with WebGL conventions.
//
// This example also demonstrates how to use a different pixel format,
// in this case, RGB.
function makeGradientTexture() {
  const size = 256;
  const typedArray = new Uint8Array(3 * size * size);
  for (let i = 0; i < size; i++) {
    for (let j = 0; j < size; j++) {
      const index = i * size + j;
      // red increases in the +x direction (to the right)
      typedArray[3 * index + 0] = j;
      // Green increases in the +y direction (from bottom to top)
      typedArray[3 * index + 1] = i;
      // blue is 0 so it is omitted.
    }
  }

  return new Cesium.TextureUniform({
    typedArray: typedArray,
    width: size,
    height: size,
    pixelFormat: Cesium.PixelFormat.RGB,
  });
}
const gradientTexture = makeGradientTexture();

// Color the texture along its UV coordinates.
const gradientShader = new Cesium.CustomShader({
  uniforms: {
    u_gradient: {
      type: Cesium.UniformType.SAMPLER_2D,
      value: gradientTexture,
    },
  },
  fragmentShaderText: `
            void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
            {
                material.diffuse = texture(u_gradient, fsInput.attributes.texCoord_0).rgb;
            }
            `,
});

// Dragging the mouse will modify the PBR values
const modifyPbrShader = new Cesium.CustomShader({
  uniforms: {
    // Vector from latest drag center to the mouse
    u_drag: {
      type: Cesium.UniformType.VEC2,
      value: new Cesium.Cartesian2(0.0, 0.0),
    },
  },
  fragmentShaderText: `
            // Click and drag to vary the PBR values
            void fragmentMain(FragmentInput vsInput, inout czm_modelMaterial material)
            {
                float dragDistance = length(u_drag);
                float variation = smoothstep(0.0, 300.0, dragDistance);
            // variation adds an golden tint to the specular highlights
                material.specular = mix(material.specular, vec3(0.8, 0.5, 0.1), variation);
            // variation makes the material glossier
                material.roughness = clamp(1.0 - variation, 0.01, 1.0);
            // variation mixes some red into the diffuse color
                material.diffuse += vec3(0.5, 0.0, 0.0) * variation;
            }
            `,
});

const pointCloudWaveShader = new Cesium.CustomShader({
  uniforms: {
    // elapsed time in seconds for animation
    u_time: {
      type: Cesium.UniformType.FLOAT,
      value: 0,
    },
  },
  vertexShaderText: `
            void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput)
            {
            // This model's x and y coordinates are in the range [0, 1], which
            // conveniently doubles as UV coordinates.
                vec2 uv = vsInput.attributes.positionMC.xy;
            // Make the point cloud undulate in a complex wave that varies in
            // both space and time. The amplitude is based on the original shape
            // of the point cloud (which already is a wavy surface). The wave
            // is computed relative to the center of the model, hence the
            // transformations from [0, 1] -> [-1, 1] -> [0, 1]
                float amplitude = 2.0 * vsInput.attributes.positionMC.z - 1.0;
                float wave = amplitude * sin(2.0 * czm_pi * uv.x - 2.0 * u_time) * sin(u_time);
                vsOutput.positionMC.z = 0.5 + 0.5 * wave;
            // Make the points pulse in and out by changing their size
                vsOutput.pointSize = 10.0 + 5.0 * sin(u_time);
            }
            `,
  fragmentShaderText: `
            void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
            {
            // Make the points circular instead of square
                float distance = length(gl_PointCoord - 0.5);
                if (distance > 0.5) {
                    discard;
                }
            // Make a sinusoid color palette that moves in the general direction
            // of the wave, but at a different speed.
            // Coefficients were chosen arbitrarily
                vec2 uv = fsInput.attributes.positionMC.xy;
                material.diffuse = 0.2 * fsInput.attributes.color_0.rgb;
                material.diffuse += vec3(0.2, 0.3, 0.4) + vec3(0.2, 0.3, 0.4) * sin(2.0 * czm_pi * vec3(3.0, 2.0, 1.0) * uv.x - 3.0 * u_time);
            }
            `,
});

// Demos ==============================================================

const models = {
  balloon: "../../SampleData/models/CesiumBalloon/CesiumBalloon.glb",
  drone: "../../SampleData/models/CesiumDrone/CesiumDrone.glb",
  pawns: "../../SampleData/models/CesiumDrone/Pawns.glb",
  milkTruck: "../../SampleData/models/CesiumMilkTruck/CesiumMilkTruck.glb",
  groundVehicle: "../../SampleData/models/GroundVehicle/GroundVehicle.glb",
  pointCloudWave: "../../SampleData/models/PointCloudWave/PointCloudWave.glb",
};

let needsDrag = false;
const demos = [
  {
    text: "Custom Texture",
    onselect: function () {
      selectModel(models.groundVehicle, textureUniformShader);
      needsDrag = false;
    },
  },
  {
    text: "Procedural Texture",
    onselect: function () {
      selectModel(models.balloon, checkerboardMaskShader);
      needsDrag = false;
    },
  },
  {
    text: "Translucent materials",
    onselect: function () {
      selectModel(models.balloon, checkerboardAlphaShader);
      needsDrag = false;
    },
  },
  {
    text: "Use Texture as Mask",
    onselect: function () {
      selectModel(models.balloon, checkerboardHolesShader);
      needsDrag = false;
    },
  },
  {
    text: "Procedural Gradient Texture",
    onselect: function () {
      selectModel(models.balloon, gradientShader);
      needsDrag = false;
    },
  },
  {
    text: "Modify PBR values via Mouse Drag",
    onselect: function () {
      selectModel(models.groundVehicle, modifyPbrShader);
      needsDrag = true;
    },
  },
  {
    text: "Expand Model via Mouse Drag",
    onselect: function () {
      selectModel(models.milkTruck, expandModelShader);
      needsDrag = true;
    },
  },
  {
    text: "Animated Point Cloud",
    onselect: function () {
      selectModel(models.pointCloudWave, pointCloudWaveShader);
      needsDrag = false;
    },
  },
];

async function selectModel(url, customShader) {
  viewer.scene.primitives.removeAll();
  try {
    const model = viewer.scene.primitives.add(
      await Cesium.Model.fromGltfAsync({
        url: url,
        customShader: customShader,
        modelMatrix: Cesium.Transforms.headingPitchRollToFixedFrame(
          position,
          hpr,
          Cesium.Ellipsoid.WGS84,
          fixedFrameTransform,
        ),
      }),
    );

    const removeListener = model.readyEvent.addEventListener(() => {
      viewer.camera.flyToBoundingSphere(model.boundingSphere, {
        duration: 0.0,
      });

      removeListener();
    });
  } catch (error) {
    console.log(`Error loading model: ${error}`);
  }
}
Sandcastle.addToolbarMenu(demos);

// Event handlers =====================================================

const startTime = performance.now();
viewer.scene.postUpdate.addEventListener(function () {
  const elapsedTimeSeconds = (performance.now() - startTime) / 1000;
  textureUniformShader.setUniform("u_time", elapsedTimeSeconds);
  pointCloudWaveShader.setUniform("u_time", elapsedTimeSeconds);
});

let dragActive = false;
const dragCenter = new Cesium.Cartesian2();

viewer.screenSpaceEventHandler.setInputAction(function (movement) {
  if (!needsDrag) {
    return;
  }

  const pickedFeature = viewer.scene.pick(movement.position);
  if (!Cesium.defined(pickedFeature)) {
    return;
  }

  viewer.scene.screenSpaceCameraController.enableInputs = false;

  // set the new drag center
  dragActive = true;
  movement.position.clone(dragCenter);
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);

const scratchDrag = new Cesium.Cartesian2();
viewer.screenSpaceEventHandler.setInputAction(function (movement) {
  if (!needsDrag) {
    return;
  }

  if (dragActive) {
    // get the mouse position relative to the center of the screen
    const drag = Cesium.Cartesian3.subtract(
      movement.endPosition,
      dragCenter,
      scratchDrag,
    );

    // Update uniforms
    expandModelShader.setUniform("u_drag", drag);
    modifyPbrShader.setUniform("u_drag", drag);
  }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

viewer.screenSpaceEventHandler.setInputAction(function (movement) {
  if (!needsDrag) {
    return;
  }

  viewer.scene.screenSpaceCameraController.enableInputs = true;

  dragActive = false;
}, Cesium.ScreenSpaceEventType.LEFT_UP);
