<script>
// Important info about performance:
// https://deck.gl/docs/developer-guide/performance#supply-attributes-directly

import { Deck, WebMercatorViewport, FlyToInterpolator, CompositeLayer } from '@deck.gl/core';
import { GeoJsonLayer, LineLayer, PathLayer, BitmapLayer, ScatterplotLayer, ColumnLayer, IconLayer } from '@deck.gl/layers';
import {HeatmapLayer} from '@deck.gl/aggregation-layers';
import { TileLayer, MVTLayer, TripsLayer } from '@deck.gl/geo-layers';
import { SimpleMeshLayer } from '@deck.gl/mesh-layers';
import { OBJLoader } from '@loaders.gl/obj';

//import { json } from 'd3-fetch';
import * as d3a from 'd3-array';

import { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved } from '@dougal/binary';
import { DougalShotLayer } from '@/lib/deck.gl';
import { DougalSequenceLayer, DougalEventsLayer } from '@/lib/deck.gl';
import DougalBinaryLoader from '@/lib/deck.gl/DougalBinaryLoader';

import { colors } from 'vuetify/lib'

function hexToArray (hex, defaultValue = [ 0xc0, 0xc0, 0xc0, 0xff ]) {

  if (typeof hex != "string" || hex.length < 6) {
    return defaultValue;
  }

  if (hex[0] == "#") {
    hex = hex.slice(1); // remove the '#' character
  }

  return [
    parseInt(hex.slice(0, 2), 16),
    parseInt(hex.slice(2, 4), 16),
    parseInt(hex.slice(4, 6), 16),
    hex.length > 6 ? parseInt(hex.slice(6, 8), 16) : 255
  ];
}

function namedColourToArray (name) {
  const parts = name.split(/\s+/).map( (s, i) =>
    i
      ? s.replace("-", "")
      : s.replace(/-([a-z])/g, (match, group1) => group1.toUpperCase())
  );
  parts[0]
  if (parts.length == 1) parts[1] = "base";
  const hex = parts.reduce((acc, key) => acc[key], colors);
  return hexToArray(hex);
}

export default {
  name: "MapLayersMixin",

  data () {

    return {

    COLOUR_SCALE_1: [
      // negative
      [65, 182, 196],
      [127, 205, 187],
      [199, 233, 180],
      [237, 248, 177],

      // positive
      [255, 255, 204],
      [255, 237, 160],
      [254, 217, 118],
      [254, 178, 76],
      [253, 141, 60],
      [252, 78, 42],
      [227, 26, 28],
      [189, 0, 38],
      [128, 0, 38]
    ]

    };

  },

  methods: {

    normalisedColourScale(v, scale = this.COLOUR_SCALE_1, min = 0, max = 1) {
      const range = max-min;
      const i = Math.min(scale.length, Math.max(Math.round((v-min) / range * scale.length), 0));
      //console.log(`v=${v}, scale.length=${scale.length}, min=${min}, max=${max}, i=${i}, → ${scale[i]}`);
      return scale[i];
    },


    makeDataFromBinary ( {positions, values, udv} ) {
      const totalCount = positions.length / 2;

      const attributes = {
        getPosition: {
          value: positions,
          type: 'float32',
          size: 2
        },
        udv
      };

      values.forEach((valArray, k) => {
        let value = valArray;
        if (valArray instanceof BigUint64Array) {
          value = Float64Array.from(valArray, v => Number(v));
        }
        attributes[`value${k}`] = {
          value,
          type: value instanceof Float64Array ? 'float64' :
                value instanceof Uint16Array ? 'uint16' :
                value instanceof Uint32Array ? 'uint32' : 'float32',
          size: 1
        };
      });

      return {
        length: totalCount,
        attributes
      };
    },

    loadOptions (options = {}) {
      return {
        loadOptions: {
          fetch: {
            method: 'GET',
            headers: {
              'Authorization': `Bearer ${this.$store.getters.jwt}`,
            }
          },
          ...options
        },
      };

    },


    osmLayer (options = {}) {
      return new TileLayer({
        id: "osm",
        // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
        data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',

        minZoom: 0,
        maxZoom: 19,
        tileSize: 256,

        renderSubLayers: props => {
          const {
            bbox: {west, south, east, north}
          } = props.tile;

          return new BitmapLayer(props, {
            data: null,
            image: props.data,
            bounds: [west, south, east, north]
          });
        },
        ...options
      })
    },


    // OSM tiles layer. Handy to make water transparent
    // but not super reliable yet

    osmVectorLayer (options = {}) {
      return new MVTLayer({
        id: 'osm',
        data: 'https://vector.openstreetmap.org/shortbread_v1/{z}/{x}/{y}.mvt',
        minZoom: 0,
        maxZoom: 14,
        getFillColor: feature => {
          const layer = feature.properties.layerName;
          //console.log("layer =", layer, feature.properties.kind);
          switch (layer) {
            case "ocean":
              return [0, 0, 0, 0];
            case "land":
              return [ 0x54, 0x6E, 0x7A, 255 ];
            default:
              return [ 240, 240, 240, 255 ];
          }
        },
        getLineColor: feature => {
          if (feature.properties.layer === 'water') {
            return [0, 0, 0, 0]; // No outline for water
          }
          return [192, 192, 192, 255]; // Default line color for roads, etc.
        },
        getLineWidth: feature => {
          if (feature.properties.highway) {
            return feature.properties.highway === 'motorway' ? 6 : 3; // Example road widths
          }
          return 1;
        },
        stroked: true,
        filled: true,
        pickable: true
      });
    },


    openSeaMapLayer (options = {}) {
      return new TileLayer({
        id: "sea",
        data: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',

        minZoom: 0,
        maxZoom: 19,
        tileSize: 256,

        renderSubLayers: props => {
          const {
            bbox: {west, south, east, north}
          } = props.tile;

          return new BitmapLayer(props, {
            data: null,
            image: props.data,
            bounds: [west, south, east, north]
          });
        },
        ...options
      })
    },


    // Norwegian nautical charts
    // As of 2025, not available for some weird reason
    nauLayer (options = {}) {
      return new TileLayer({
        id: "nau",
        // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
        data: 'https://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=sjokartraster&zoom={z}&x={x}&y={y}',

        minZoom: 0,
        maxZoom: 19,
        tileSize: 256,

        renderSubLayers: props => {
          const {
            bbox: {west, south, east, north}
          } = props.tile;

          return new BitmapLayer(props, {
            data: null,
            image: props.data,
            bounds: [west, south, east, north]
          });
        },
        ...options
      })
    },

    vesselTrackPointsLayer (options = {}) {

      if (!this.vesselPosition) return;

      return new SimpleMeshLayer({
        id: 'navp',
        data: [ this.vesselPosition ],
        //getColor: [ 255, 48, 0 ],
        getColor: [ 174, 1, 174 ],
        getOrientation: d => [0, (270 - (d.heading ?? d.cmg ?? d.bearing ?? d.lineBearing ?? 0)) % 360 , 0],
        getPosition: d => [ d.x, d.y ],
        mesh: `/assets/boat0.obj`,
        sizeScale: 0.1,
        loaders: [OBJLoader],
        pickable: true,
        ...options
      });

    },

    vesselTrackLinesLayer (options = {}) {

      const cfg = this.vesselTrackPeriodSettings[this.vesselTrackPeriod];

      let ts1 = new Date(this.vesselTrackLastRefresh*1000);
      let ts0 = new Date(ts1.valueOf() - cfg.offset);
      let di = cfg.decimation;
      let l = 10000;

      const breakLimit = (di ? di*20 : 5 * 60) * 1000;

      let trailLength = (ts1 - ts0) / 1000;

      return new TripsLayer({
        id: 'navl',
        data: `/api/vessel/track/?di=${di}&l=${l}&project=&ts0=${ts0.toISOString()}&ts1=${ts1.toISOString()}`,
        ...this.loadOptions({
          fetch: {
            method: 'GET',
            headers: {
              Authorization: `Bearer ${this.$store.getters.jwt}`,
            }
          }
        }),
        dataTransform: (data) => {
            if (data.length >= l) {
              console.warn(`Vessel track data may be truncated! Limit: ${l}`);
            }

            const paths = [];
            let prevTstamp;
            paths.push({path: [], timestamps: [], num: 0, ts0: +Infinity, ts1: -Infinity});
            for (const el of data) {
              const tstamp = new Date(el.tstamp).valueOf();
              const curPath = () => paths[paths.length-1];
              if (prevTstamp && Math.abs(tstamp - prevTstamp) > breakLimit) {
                // Start a new path
                console.log(`Breaking path on interval ${Math.abs(tstamp - prevTstamp)} > ${breakLimit}`);
                paths.push({path: [], timestamps: [], num: paths.length, ts0: +Infinity, ts1: -Infinity});
              }

              if (tstamp < curPath().ts0) {
                curPath().ts0 = tstamp;
              }
              if (tstamp > curPath().ts1) {
                curPath().ts1 = tstamp;
              }

              curPath().path.push([el.x, el.y]);
              curPath().timestamps.push(tstamp/1000);
              prevTstamp = tstamp;
            }

            paths.forEach (path => {
              path.nums = paths.length;
              path.ts0 = new Date(path.ts0);
              path.ts1 = new Date(path.ts1);
            });

            return paths;
        },
        getPath: d => d.path,
        getTimestamps: d => d.timestamps,
        currentTime: ts1.valueOf() / 1000,
        trailLength,
        widthUnits: "meters",
        widthMinPixels: 4,
        getWidth: 10,
        getColor: [ 174, 1, 126, 200 ],
        stroked: true,
        pickable: true,
        ...options
      });

    },

    eventsLogLayer (options = {}) {

      const labelColour = (d, i, t, c = [127, 65, 90]) => {
        const label = d?.properties?.labels?.[0];
        const colour = this.labels[label]?.view?.colour ?? "#cococo";

        if (colour) {
          if (colour[0] == "#") {
            c = hexToArray(colour);
          } else {
            c = namedColourToArray(colour);
          }
        } else {
          //return [127, 65, 90];
        }

        if (t != null) {
          c[3] = t;
        }

        return c;
      };

      return new DougalEventsLayer({
        id: 'log',
        data: `/api/project/${this.$route.params.project}/event?mime=application/geo%2Bjson`,
        ...this.loadOptions(),
        lineWidthMinPixels: 2,
        getPosition: d => d.geometry.coordinates,
        jitter: 0.00015,
        getElevation: d => Math.min(Math.max(d.properties.remarks?.length || 10, 10), 200),
        getFillColor: (d, i) => labelColour(d, i, 200),
        getLineColor: (d, i) => labelColour(d, i, 200),
        radius: 0.001,
        radiusScale: 1,
        // This just won't work with radiusUnits = "pixels".
        // See: https://grok.com/share/c2hhcmQtMw%3D%3D_16578be4-20fd-4000-a765-f082503d0495
        radiusUnits: "pixels",
        radiusMinPixels: 1.5,
        radiusMaxPixels: 2.5,

        pickable: true,
        ...options
      })

    },

    preplotSaillinesLinesLayer (options = {}) {
      return new GeoJsonLayer({
        id: 'psll',
        data: `/api/project/${this.$route.params.project}/gis/preplot/line?class=V&v=${this.lineTStamp?.valueOf()}`,
        ...this.loadOptions(),
        lineWidthMinPixels: 1,
        getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
        getLineWidth: 1,
        getPointRadius: 2,
        radiusUnits: "pixels",
        pointRadiusMinPixels: 2,
        pickable: true,
        ...options
      })
    },

    preplotLinesLayer (options = {}) {
      return new GeoJsonLayer({
        id: 'ppll',
        data: `/api/project/${this.$route.params.project}/gis/preplot/line?v=${this.lineTStamp?.valueOf()}`,
        ...this.loadOptions(),
        lineWidthMinPixels: 1,
        getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
        getLineWidth: 1,
        getPointRadius: 2,
        radiusUnits: "pixels",
        pointRadiusMinPixels: 2,
        pickable: true,
        ...options
      })
    },

    plannedLinesLinesLayer (options = {}) {
      return new PathLayer({
        id: 'planl',
        data: [...this.plannedSequences], // Create new array to trigger Deck.gl update
        dataTransform: (sequences) => {
          // Raise the data 10 m above ground so that it's visible over heatmaps, etc.
          return sequences.map( seq => ({
            ...seq,
            geometry: {
              ...seq.geometry,
              coordinates: seq.geometry.coordinates.map( pos => [...pos, 10] )
            }
          }))
        },
        getPath: d => d.geometry.coordinates,
        //getSourcePosition: d => d.geometry.coordinates[0],
        //getTargetPosition: d => d.geometry.coordinates[1],
        widthUnits: "meters",
        widthMinPixels: 4,
        getWidth: 25,
        //getLineWidth: 10,
        getColor: (d) => {
          const k = (d?.azimuth??0)/360*255;
          return [ k, 128, k, 200 ];
        },
        stroked: true,
        pickable: true,
        ...options
      });
    },

    rawSequencesLinesLayer (options = {}) {
      return new GeoJsonLayer({
        id: 'seqrl',
        data: `/api/project/${this.$route.params.project}/gis/raw/line?v=${this.sequenceTStamp?.valueOf()}`,
        ...this.loadOptions(),
        lineWidthMinPixels: 1,
        getLineColor: (d) => d.properties.ntbp ? [0xe6, 0x51, 0x00, 200] : [0xff, 0x98, 0x00, 200],
        getLineWidth: 1,
        getPointRadius: 2,
        radiusUnits: "pixels",
        pointRadiusMinPixels: 2,
        pickable: true,
        ...options
      })
    },

    finalSequencesLinesLayer (options = {}) {
      return new GeoJsonLayer({
        id: 'seqfl',
        data: `/api/project/${this.$route.params.project}/gis/final/line?v=${this.sequenceTStamp?.valueOf()}`,
        ...this.loadOptions(),
        lineWidthMinPixels: 1,
        getLineColor: (d) => d.properties.pending ? [0xa7, 0xff, 0xab, 200] : [0x00, 0x96, 0x88, 200],
        getLineWidth: 1,
        getPointRadius: 2,
        radiusUnits: "pixels",
        pointRadiusMinPixels: 2,
        pickable: true,
        ...options
      })
    },

    preplotSaillinesPointLayer (options = {}) {
      return new DougalSequenceLayer({
        id: 'pslp',
        data: `/api/project/${this.$route.params.project}/line/sail?v=${this.lineTStamp?.valueOf()}`, // API endpoint returning binary data
        loaders: [DougalBinaryLoader],
        ...this.loadOptions({
          fetch: {
            method: 'GET',
            headers: {
              Authorization: `Bearer ${this.$store.getters.jwt}`,
              Accept: 'application/vnd.aaltronav.dougal+octet-stream'
            }
          }
        }),
        getRadius: 2,
        getFillColor: (d, {data, index}) => data.attributes.value2.value[index] ? [240, 248, 255, 200] : [85, 170, 255, 200],
        //getFillColor: [0, 120, 220, 200],
        pickable: true,
        ...options
      });
    },

    preplotPointsLayer (options = {}) {
      return new DougalSequenceLayer({
        id: 'pplp',
        data: `/api/project/${this.$route.params.project}/line/source?v=${this.lineTStamp?.valueOf()}`, // API endpoint returning binary data
        loaders: [DougalBinaryLoader],
        ...this.loadOptions({
          fetch: {
            method: 'GET',
            headers: {
              Authorization: `Bearer ${this.$store.getters.jwt}`,
              Accept: 'application/vnd.aaltronav.dougal+octet-stream'
            }
          }
        }),
        getRadius: 2,
        getFillColor: (d, {data, index}) => data.attributes.value2.value[index] ? [240, 248, 255, 200] : [85, 170, 255, 200],
        //getFillColor: [0, 120, 220, 200],
        pickable: true,
        ...options
      });
    },

    plannedLinesPointsLayer (options = {}) {
    },

    rawSequencesPointsLayer (options = {}) {

      return new DougalSequenceLayer({
        id: 'seqrp',
        data: this.makeDataFromBinary(this.sequenceBinaryData),
        getRadius: 2,
        getFillColor: [0, 120, 220, 200],
        pickable: true,
        ...options
      });
    },

    finalSequencesPointsLayer (options = {}) {

      return new DougalSequenceLayer({
        id: 'seqfp',
        data: this.makeDataFromBinary(this.sequenceBinaryDataFinal),
        getRadius: 2,
        getFillColor: [220, 120, 0, 200],
        pickable: true,
        ...options
      });
    },

    heatmapLayer(options = {}) {
      const { positions, values } = this.heatmapValue?.startsWith("co_")
        ? this.sequenceBinaryDataFinal
        : this.sequenceBinaryData;

      if (!positions?.length || !values?.length) {
        console.warn('No valid data for heatmapLayer');

        return new HeatmapLayer({
          id: 'seqrh',
          data: [],
          ...options
        });
      }


      let weights, offset = 0, scaler = 1;
      let colorDomain = null;
      let aggregation = "MEAN";
      let transform = (v) => v;

      switch (this.heatmapValue) {
        case "total_error":
          weights = Float32Array.from(values[3], (ei, i) => {
            const ej = values[4][i];
            return Math.sqrt(ei * ei + ej * ej) / 100; // Euclidean distance in meters
          });
          colorDomain = [2, 20]; // scale: 1 (already divided by 100 above)
          break;
        case "delta_i":
          weights = values[3];
          scaler = 0.1;
          colorDomain = [100, 1200]; // scale: 100 (1 ‒ 12 m)
          break;
        case "delta_j":
          weights = values[4];
          scaler = 0.1;
          colorDomain = [10, 80]; // scale: 100 (0.1 ‒ 0.8 m)
          break;

        case "co_total_error":
          weights = Float32Array.from(values[3], (ei, i) => {
            const ej = values[4][i];
            return Math.sqrt(ei * ei + ej * ej) / 100; // Euclidean distance in meters
          });
          colorDomain = [10, 150]; // Scale: 100 (0.1 ‒ 1 m)
          break;
        case "co_delta_i":
          weights = values[5];
          scaler = 0.1;
          colorDomain = [10, 150];
          break;
        case "co_delta_j":
          weights = values[6];
          scaler = 0.1;
          colorDomain = [0.2, 2];
          break;

        case "delta_μ":
          weights = values[5];
          scaler = 0.1;
          break;
        case "delta_σ":
          weights = values[6];
          scaler = 0.1;
          colorDomain = [ 0.1, 1.5 ];
          break;
        case "delta_R":
          weights = values[7];
          scaler = 0.1;
          colorDomain = [ 0.5, 1.0 ];
          break;
        case "press_μ":
          weights = values[8];
          offset = -2000;
          colorDomain = [ 5, 50 ];
          break;
        case "press_σ":
          weights = values[9];
          colorDomain = [ 1, 19 ];
          break;
        case "press_R":
          weights = values[10];
          colorDomain = [ 3, 50 ];
          break;
        case "depth_μ":
          weights = values[11];
          offset = -6;
          scaler = 0.1;
          colorDomain = [ 0.1, 1 ];
          break;
        case "depth_σ":
          weights = values[12];
          scaler = 0.1;
          break;
        case "depth_R":
          weights = values[13];
          scaler = 0.1;
          break;
        case "fill_μ":
          weights = values[14];
          colorDomain = [ 300, 1000 ];
          break;
        case "fill_σ":
          weights = values[15];
          offset = -250;
          colorDomain = [ 0, 250 ];
          break;
        case "fill_R":
          weights = values[16];
          offset = -500;
          colorDomain = [ 0, 500 ];
          break;
        case "delay_μ":
          weights = values[17];
          offset = -150;
          colorDomain = [ 1.5, 25 ];
          //transform = (v) => {console.log("τ(v)", v); return v;};
          break;
        case "delay_σ":
          weights = values[18];
          break;
        case "delay_R":
          weights = values[19];
          break;
        case "no_fire":
          weights = values[20];
          transform = v => v >> 4;
          aggregation = "SUM";
          colorDomain = [ 0.1, 1.5 ];
          break;
        case "autofire":
          weights = values[20];
          transform = v => v & 0xf;
          aggregation = "SUM";
          colorDomain = [ 0.5, 1.5 ];
          break;
        case "misfire":
          weights = values[20];
          aggregation = "SUM";
          colorDomain = [ 0.5, 1.5 ];
          break;
      }


      const stats = {
        min: d3a.min(weights),
        mode: d3a.mode(weights),
        mean: d3a.mean(weights),
        max: d3a.max(weights),
        sd: d3a.deviation(weights),
      };
      const sr0 = [ stats.mean - 2.1*stats.sd, stats.mean + 2.1*stats.sd ];
      const sr1 = [ stats.mode - 2.1*stats.sd, stats.mode + 2.1*stats.sd ];

      /*
      console.log('Positions sample:', positions.slice(0, 10));
      console.log('Weights sample:', weights.slice(0, 10));
      console.log("Mode:", this.heatmapValue);
      console.log('Weight stats:', stats);
      console.log("Suggested ranges");
      console.log(sr0);
      console.log(sr1);
      console.log("Actual ranges");
      console.log(colorDomain);
      */

      return new HeatmapLayer({
        id: 'seqrh',
        data: {
          length: weights.length,
          positions,
          weights
          /*
          attributes: {
            getPosition: { value: positions, type: 'float32', size: 2 },
            getWeight: { value: weights, type: 'float32', size: 1 }
          }
          */
        },
        getPosition: (d, {index, data}) => [ data.positions[index*2], data.positions[index*2+1] ],
        getWeight: (d, {index, data}) => transform(Math.abs(data.weights[index] * scaler + offset)),
        colorDomain,
        radiusPixels: 25,
        aggregation,
        pickable: false,
        ...options
      });
    },

  },

}

</script>
