Snippets Collections
from bs4 import BeautifulSoup
import requests
import json

# Initialize the list to store API data
api = []

# Base URL for Yahoo News
base_url = 'https://www.yahoo.com'

# Fetch the main page
url = f'{base_url}/news/'
response = requests.get(url)

# Check if the request was successful
if response.status_code == 200:
    soup = BeautifulSoup(response.text, 'html.parser')
    
    # Find all news items
    for news_item in soup.find_all('ul', class_='stream-items'):
        for item in news_item.find_all('li', class_='stream-item'):
            
            # Extract the article link
            item_id = item.find('a', class_='js-content-viewer')
            if item_id:
                link = item_id.get('href')
                full_link = f'{base_url}{link}'
                
                # Fetch the article page
                response2 = requests.get(full_link)
                if response2.status_code == 200:
                    soup2 = BeautifulSoup(response2.text, 'html.parser')
                    
                    # Extract article details
                    itemInfo = soup2.find('div', class_='caas-inner-body')
                    if itemInfo:
                        text1 = ''
                        for text in itemInfo.find_all('div', class_='caas-body'):
                            text1 += text.text

                        # Remove "View comments" from the text
                        text1 = text1.replace("View comments", "").strip()

                        # Extract additional details if available
                        image = item.find('img')['src'] if item.find('img') else ''
                        category = item.find('strong', class_='Tt(c)').text if item.find('strong', class_='Tt(c)') else ''
                        ell = item.find('span', class_='Ell').text if item.find('span', class_='Ell') else ''
                        title = item.find('h3', class_='stream-item-title').text if item.find('h3', class_='stream-item-title') else ''
                        description = item.find('p', class_='finance-ticker-fetch-success_D(n)').text if item.find('p', class_='finance-ticker-fetch-success_D(n)') else ''
                        
                        # Append data to the api list
                        api.append({
                            'link': full_link,
                            'image': image,
                            'category': category,
                            'ell': ell,
                            'title': title,
                            'description': description,
                            'text': text1,
                        })

# Convert the api list to a JSON-formatted string
api_json = json.dumps(api, indent=4)

# Write the JSON data to a file
with open('news_data.json', 'w') as file:
    file.write(api_json)

print("Data has been written to news_data.json")
            {data.map((item, index) => (
              <div key={index} className=" overflow-hidden w-[50%] sm:w-1/2 md:w-1/3 lg:w-1/4">
                {item.image_url  && (
                  <div 
                    className="w-[300px] relative h-56 bg-cover bg-center rounded-2xl before:bg-black/30 before:rounded-2xl before:absolute before:inset-0 before:content-['']" 
                    style={{ backgroundImage: `url(${item.image_url})` }}
                    aria-label={item.title}
                  >
                    <div className="absolute bottom-0 right-0 p-4  flex justify-end items-end text-white w-full h-full ">
                      <p className="hover:underline font-semibold">{item.title}</p>
                    </div>
                  </div>
                )}
               
              </div>
            ))}
<div class="absolute bottom-8 left-2 z-1 gmnoprint" role="menubar" :style="{ margin: '5px', zIndex: 1 }">
        <div :style="{ float: 'left', lineHeight: 0 }">
          <button
            id="stopdraw"
            draggable="false"
            aria-label="Stop drawing"
            title="Stop drawing"
            type="button"
            role="menuitemradio"
            aria-checked="true"
            :style="{
              background: ' none padding-box rgb(255, 255, 255)',
              display: ' block',
              border: ' 0px',
              margin: ' 0px',
              padding: ' 4px',
              textTransform: ' none',
              appearance: ' none',
              position: ' relative',
              cursor: ' pointer',
              userSelect: ' none',
              direction: ' ltr',
              overflow: ' hidden',
              textAlign: ' left',
              color: ' rgb(0, 0, 0)',
              fontFamily: ' Roboto, Arial, sans-serif',
              fontSize: ' 11px',
              borderBottomLeftRadius: ' 2px',
              borderTopLeftRadius: ' 2px',
              boxShadow: ' rgba(0, 0, 0, 0.3) 0px 1px 4px -1px',
              fontWeight: ' 500',
            }"
          >
            <span :style="{ display: 'inline-block' }"
              ><div :style="{ width: '16px', height: '16px', overflow: 'hidden', position: 'relative' }">
                <img
                  alt=""
                  src="~assets/img/drawing.png"
                  draggable="false"
                  :style="{
                    position: 'absolute',
                    left: '0px',
                    top: '-144px',
                    userSelect: 'none',
                    border: '0px',
                    padding: '0px',
                    margin: '0px',
                    maxWidth: 'none',
                    width: '16px',
                    height: '192px',
                  }"
                /></div
            ></span>
          </button>
        </div>

        <div :style="{ float: 'left', lineHeight: 0 }">
          <button
            id="startdraw"
            draggable="false"
            aria-label="Draw a shape"
            title="Draw a shape"
            type="button"
            role="menuitemradio"
            aria-checked="false"
            :style="{
              background: ' none padding-box rgb(255, 255, 255)',
              display: ' block',
              border: ' 0px',
              margin: ' 0px',
              padding: ' 4px',
              textTransform: ' none',
              appearance: ' none',
              position: ' relative',
              cursor: ' pointer',
              userSelect: ' none',
              direction: ' ltr',
              overflow: ' hidden',
              textAlign: ' left',
              color: ' rgb(86, 86, 86)',
              fontFamily: ' Roboto, Arial, sans-serif',
              fontSize: ' 11px',
              boxShadow: ' rgba(0, 0, 0, 0.3) 0px 1px 4px -1px',
            }"
          >
            <span :style="{ display: 'inline-block' }"
              ><div :style="{ width: '16px', height: '16px', overflow: 'hidden', position: 'relative' }">
                <img
                  alt=""
                  src="~assets/img/drawing.png"
                  draggable="false"
                  :style="{
                    position: 'absolute',
                    left: '0px',
                    top: '-64px',
                    userSselect: 'none',
                    border: '0px',
                    padding: '0px',
                    margin: '0px',
                    maxWidth: 'none',
                    width: '16px',
                    height: '192px',
                  }"
                /></div
            ></span>
          </button>
        </div>
      </div>
 const loader = new Loader({
      apiKey: //your_api_key,
      version: "weekly",
      libraries: ["places"],
    });
const { Map, OverlayView } = await loader.importLibrary("maps");    
function getAddress(place) {
  const { address_components, formatted_address, geometry, name } = place;
  let country = (address_components || []).find((component) => component.types.includes("country"));
  let state = (address_components || []).find((component) =>
    component.types.includes("administrative_area_level_1")
  );
  let city = (address_components || []).find((component) => component.types.includes("locality"));
  let address_line = (address_components || []).find((component) => component.types.includes("route"));
  let postal_code = (address_components || []).find((component) => component.types.includes("postal_code"));
  return {
    name: name,
    address: formatted_address,
    country: country?.long_name,
    city: city?.long_name,
    state: state?.long_name,
    postalCode: postal_code?.long_name,
    coords: {
      lat: geometry.location.lat(),
      lng: geometry.location.lng(),
    },
  };
}
class CustomOverlay extends OverlayView {
      div_;
      constructor(map) {
        super();
        this.div_ = null;
        this.setMap(map);
      }
      async onAdd() {
        var div = document.createElement("div");
        div.style.zIndex = "100";
        div.style.borderStyle = "solid";
        div.style.borderWidth = "1px";
        div.style.backgroundColor = "white";
        div.style.padding = "4px";
        div.style.position = "absolute";
        div.style.height = "40px";

        // Add a textbox inside the div
        var textbox = document.createElement("input");
        textbox.type = "text";
        textbox.style.width = "100%";
        textbox.style.height = "100%";
        textbox.placeholder = "Search Location";
        div.appendChild(textbox);
        this.div_ = div;
        this.getPanes().overlayMouseTarget.appendChild(div);
        // this.getPanes().floatPane.appendChild(div);

        // Prevent map from intercepting events when interacting with the textbox
        function handleMouseEvents(e) {
          if (e.target == textbox) {
            e.stopPropagation();
          }
        }
        const map = this.getMap();
        const mapDiv = map.getDiv();
        google.maps.event.addDomListener(mapDiv, "mousedown", handleMouseEvents, true);
        google.maps.event.addDomListener(mapDiv, "dblclick", handleMouseEvents, true);

        map.addListener("dragstart", function (e) {
          //
        });
        map.addListener("dragend", function (e) {});
        const { SearchBox } = await loader.importLibrary("places");

        const search = new SearchBox(textbox);
        search.addListener("places_changed", async function () {
          const place = search.getPlaces();
          console.log("use data for the place", place);
          // or use the function above "getAddress" to return a json formatted data
        });
      }
      draw() {
        var overlayProjection = this.getProjection();
        if (!overlayProjection) {
          return;
        }

        // Get northeast and southwest corners of the map's current bounds
        const mapBounds = this.getMap().getBounds();
        const fromLatLngToDivPixel = function (latLng) {
          return overlayProjection.fromLatLngToDivPixel(latLng);
        };

        const ne = fromLatLngToDivPixel(mapBounds.getNorthEast());
        const sw = fromLatLngToDivPixel(mapBounds.getSouthWest());
        if (this.div_) {
          this.div_.style.left = sw.x + 10 + "px";
          this.div_.style.top = ne.y + 10 + "px";
        }
      }
      onRemove() {
        if (!this.div_) return;
        this.div_.parentNode.removeChild(this.div_);
        this.div_ = null;
      }
    }
	// load a maps library and pass it to the class
    
    const map = new Map(vm.$refs.GoogleMap, {
      zoom: 14,
      disableDefaultUI: false,
      mapTypeControl: false,
      streetViewControl: false,
      mapTypeId: "roadmap",
      scrollWheel: true,
      fullscreenControl: true,
      center: {
        lat: 49.316666,
        lng: -123.066666,
      },
    });
	
    new CustomOverlay(map);
const input = document.getElementById('input');


const debounc = (func, waitTime) => {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            func(...args);
        }, waitTime);
    };
}

function getData(e) {
    console.log(e.target.value)
};

const debouncApi = debounc(getData, 1000);


input.addEventListener('input', debouncApi);
function getTransformHandleFun(ele) {
    if (!(ele instanceof HTMLElement || ele instanceof Node)) throw new Error("Note element or node");
    const clamp = (value, min, max) => {
        if (typeof min === "number" && typeof max === "number") {
            return Math.min(Math.max(value, min), max);
        } else if (typeof min === "number" && typeof max !== "number") {
            return Math.max(value, min)
        } else if (typeof min !== "number" && typeof max === "number") {
            return Math.min(value, max)
        }
        return value
    }

    const setTranslate = (options) => {
        if (typeof options === "function") options = options();
        const { isDeff = true } = options;
        const o = { value: 0 }, _ = isDeff ? o : { value: 1 };
        const { dA = _, dB = o, dC = o, dD = _, dTx = o, dTy = o } = options;
        console.log(dA, dB);

        const style = getComputedStyle(ele);
        let transform = style.transform || style.webkitTransform || style.mozTransform;
        if (transform === 'none') transform = "matrix(1, 0, 0, 1, 0, 0)";

        if (transform) {
            const matrixValues = transform.match(/^matrix.*\((.+)\)$/);
            if (!matrixValues) return "matrix(1, 0, 0, 1, 0, 0)";
            const vals = matrixValues[1].split(', ').map(Number);
            const arr = [dA, dB, dC, dD, dTx, dTy];
            while (vals.length < 6) vals.push(0);
            let getVal = isDeff ? (i) => +arr[i].value + vals[i] : (i) => +arr[i].value;
            for (let i = 0; i < vals.length; i++) {
                vals.splice(i, 1, clamp(getVal(i), arr[i].min, arr[i].max))
            }
            transform = `matrix(${vals})`;
        }

        return transform;
    }

    return (options) => {
        ele.style.transform = setTranslate(options);
        return ele;
    }
};
function throttle(fun, delay) {
    let isRun = true;
    return function (...args) {
        if (isRun) {
            fun.apply(this, args)
            isRun = false
            setTimeout(() => {
                isRun = true
            }, delay)
        }
    }
}
function debounce(fun, delay) {
    let i;
    return function (...args) {
        if (i) clearTimeout(i)
        setTimeout(() => {
            fun.apply(this, args)
        }, delay);
    }
}
function hasChanged(x, y) {
    if (x === y) return x === 0 && 1 / x !== 1 / y
    else return x === x || y === y
}
; (function () {
    const ob = new IntersectionObserver((entries) => {
        entries.filter(entry => entry.isIntersecting).forEach(entry => {
            const img = entry.target;
            img.src = img.dataset.src;
            ob.unobserve(img);
        })
    })

    const imgs = document.querySelectorAll('img[data-src]');
    imgs.forEach(img => { ob.observe(img) });
})();
<!-- Squarepaste Form Logic © -->

<script src="https://storage.googleapis.com/squarepaste/base-jquery.js"></script>

<script type="text/javascript">
/* Select Field */

$(document).on('change', '#select-6ed7f446-8ef8-42ea-abf4-e72a84b41cb3-field select', function() {

        const value = $(this).val();

        if (value == 'Yes') {

           $('#section-8ce22bc1-00cb-43c0-b3ab-cccaab43ad22').fadeIn();

        }

        else {

            $('#section-8ce22bc1-00cb-43c0-b3ab-cccaab43ad22').hide();

        }

    });

/* Radio Field- Hide submit button based on radio button selection and display message */
    $(document).on('change', 'input[type="radio"]', function() {
        if ($('input[type="radio"][value="No"]:checked').length > 0) {
            $('button[type="submit"]').fadeIn();
            $('#section-26b8b3d8-8295-42a8-bae3-3f26b85de441').hide();
        } else if ($('input[type="radio"][value="No"]:checked').length === 0) {
            $('button[type="submit"]').hide();
            $('#section-26b8b3d8-8295-42a8-bae3-3f26b85de441').fadeIn();

        } else {
            $('button[type="submit"]').hide();
        }
    });

    // Trigger the change event on page load to set initial state
    $('input[type="radio"]:checked').change();

    

</script>
let name = "Ilya";

alert( `hello ${1}` ); // ?

alert( `hello ${"name"}` ); // ?

alert( `hello ${name}` ); // ?
const animals = ['hippo', 'tiger', 'lion', 'seal', 'cheetah', 'monkey', 'salamander', 'elephant'];

const foundAnimal = animals.findIndex(a => {
  return a === 'elephant';
});
console.log(animals[foundAnimal]); // uses function's returned index to display value

const startsWithS = animals.findIndex(letter => {
  return letter[0] === 's';
});
console.log(startsWithS); // function returns index number of first TRUE element
console.log(animals[startsWithS]); // used to display that element's value
const favoriteWords = ['nostalgia', 'hyperbole', 'fervent', 'esoteric', 'serene'];

// Call .filter() on favoriteWords below

const longFavoriteWords = favoriteWords.filter(word => {
  return word.length > 7;
});

console.log(longFavoriteWords);
function myFunction() {
  const ss = SpreadsheetApp.getActiveSpreadsheet()
  const sh = ss.getSheetByName("Sheet1");
  const hex_colors = sh.getRange('A1:A'+sh.getLastRow()).getValues();
  sh.getRange('B1:B'+sh.getLastRow()).setBackgrounds(hex_colors);
}
// regex for hex color codes
HEX_COLOR_REGEX = /(^#[0-9A-Fa-f]{3}$)|(#[0-9A-Fa-f]{6}$)/;

// column to watch for changes (i.e. column where hex color codes are to be entered)
HEX_CODE_COLUMN = 1; // i.e. column A

// column to change when above column is edited
HEX_COLOR_COLUMN = 2; // i.e. column B

// utility function to test whether a given string qualifies as a hex color code
function hexTest(testCase) {
  return HEX_COLOR_REGEX.test(testCase);
}

function onEdit(e) {
  var range = e.range;
  var row = range.getRow();
  var column = range.getColumn();
  if (column === HEX_CODE_COLUMN) {
    var values = range.getValues();
    values.forEach( function checkCode(rowValue, index) {
      var code = rowValue[0];
      if (hexTest(code)) {
        var cell = SpreadsheetApp.getActiveSheet().getRange(row + index, HEX_COLOR_COLUMN);
        cell.setBackground(code);
        SpreadsheetApp.flush();
      }
    });
  }
}
function onEdit() {

  var sheet = SpreadsheetApp.getActiveSheet();
  var range = sheet.getDataRange();
  var actCell = sheet.getActiveCell();
  var actData = actCell.getValue();
  var actRow = actCell.getRow();
  if (actData != '' && actRow != 1)  //Leaving out empty and header rows
  {
    range.getCell(actRow, 2).setBackground(actData);
  }

}
/*

This script is meant to be used with a Google Sheets spreadsheet. When you edit a cell containing a
valid CSS hexadecimal colour code (like #000 or #000000), the background colour will be changed to
that colour and the font colour will be changed to the inverse colour for readability.

To use this script in a Google Sheets spreadsheet:
1. go to Tools » Script Editor » Spreadsheet;
2. erase everything in the text editor;
3. change the title to "Set colour preview on edit";
4. paste this code in;
5. click File » Save.
*/

/*********
** Properties
*********/
/**
 * A regex pattern matching a valid CSS hex colour code.
 */
var colourPattern = /^#([0-9a-f]{3})([0-9a-f]{3})?$/i;


/*********
** Event handlers
*********/
/**
 * Sets the foreground or background color of a cell based on its value.
 * This assumes a valid CSS hexadecimal colour code like #FFF or #FFFFFF.
 */
function onEdit(e){
  // iterate over cell range  
  var range = e.range;
  var rowCount = range.getNumRows();
  var colCount = range.getNumColumns();
  for(var r = 1; r <= rowCount; r++) {
    for(var c = 1; c <= colCount; c++) {
      var cell = range.getCell(r, c);
      var value = cell.getValue();

      if(isValidHex(value)) {
        cell.setBackground(value);
        cell.setFontColor(getContrastYIQ(value));
      }
      else {
        cell.setBackground('white');
        cell.setFontColor('black');
      }
    }
  }
};


/*********
** Helpers
*********/
/**
 * Get whether a value is a valid hex colour code.
 */
function isValidHex(hex) {
  return colourPattern.test(hex);
};

/**
 * Change text color to white or black depending on YIQ contrast
 * https://24ways.org/2010/calculating-color-contrast/
 */
function getContrastYIQ(hexcolor){
    var r = parseInt(hexcolor.substr(1,2),16);
    var g = parseInt(hexcolor.substr(3,2),16);
    var b = parseInt(hexcolor.substr(5,2),16);
    var yiq = ((r*299)+(g*587)+(b*114))/1000;
    return (yiq >= 128) ? 'black' : 'white';
}
const fruits = ['mango', 'papaya', 'pineapple', 'apple'];

// Iterate over fruits below
fruits.forEach(item => console.log(`I want to eat a ${item}.`));
const addTwo = num => {
  return num + 2;
}

const checkConsistentOutput = (func, val) => {
  let checkA = val + 2;
  let checkB = func(val);
  if (checkA === checkB) {
    return checkB;
  } else {
    console.log(`inconsistent results`);
  }
}

console.log(checkConsistentOutput(addTwo, 2));
let cupsOfSugarNeeded = 2;
let cupsAdded = 0;

do {
  cupsOfSugarNeeded = cupsOfSugarNeeded - cupsAdded;
  cupsAdded++;
  console.log(`Needed: ${cupsOfSugarNeeded}\n Added: ${cupsAdded}`);
} while (cupsAdded < cupsOfSugarNeeded);
/*

This script is meant to be used with a Google Sheets spreadsheet. When you edit a cell containing a
valid CSS hexadecimal colour code (like #000 or #000000), the background colour will be changed to
that colour and the font colour will be changed to the inverse colour for readability.

To use this script in a Google Sheets spreadsheet:
1. go to Tools » Script Editor » Spreadsheet;
2. erase everything in the text editor;
3. change the title to "Set colour preview on edit";
4. paste this code in;
5. click File » Save.
*/

/*********
** Properties
*********/
/**
 * A regex pattern matching a valid CSS hex colour code.
 */
var colourPattern = /^#([0-9a-f]{3})([0-9a-f]{3})?$/i;


/*********
** Event handlers
*********/
/**
 * Sets the foreground or background color of a cell based on its value.
 * This assumes a valid CSS hexadecimal colour code like #FFF or #FFFFFF.
 */
function onEdit(e){
  // iterate over cell range  
  var range = e.range;
  var rowCount = range.getNumRows();
  var colCount = range.getNumColumns();
  for(var r = 1; r <= rowCount; r++) {
    for(var c = 1; c <= colCount; c++) {
      var cell = range.getCell(r, c);
      var value = cell.getValue();

      if(isValidHex(value)) {
        cell.setBackground(value);
        cell.setFontColor(getContrastYIQ(value));
      }
      else {
        cell.setBackground('white');
        cell.setFontColor('black');
      }
    }
  }
};


/*********
** Helpers
*********/
/**
 * Get whether a value is a valid hex colour code.
 */
function isValidHex(hex) {
  return colourPattern.test(hex);
};

/**
 * Change text color to white or black depending on YIQ contrast
 * https://24ways.org/2010/calculating-color-contrast/
 */
function getContrastYIQ(hexcolor){
    var r = parseInt(hexcolor.substr(1,2),16);
    var g = parseInt(hexcolor.substr(3,2),16);
    var b = parseInt(hexcolor.substr(5,2),16);
    var yiq = ((r*299)+(g*587)+(b*114))/1000;
    return (yiq >= 128) ? 'black' : 'white';
}
app.get(%27/ab?cd%27, (req, res) => {
  res.send(%27ab?cd%27)
})
app.get(%27/random.text%27, (req, res) => {
  res.send(%27random.text%27)
})
app.get(%27/about%27, (req, res) => {
  res.send(%27about%27)
})
app.get(%27/%27, (req, res) => {
  res.send(%27root%27)
})
app.all(%27/secret%27, (req, res, next) => {
  console.log(%27Accessing the secret section ...%27)
  next() // pass control to the next handler
})
// GET method route
app.get('/', (req, res) => {
  res.send('GET request to the homepage')
})

// POST method route
app.post('/', (req, res) => {
  res.send('POST request to the homepage')
})
Embark on a lucrative journey with our turnkey vacation rental business – your golden ticket to making money while providing unforgettable travel experiences! Just like Airbnb, We at Appticz develop a vacation rental business that empowers you to transform your property into a revenue-generating oasis. Imagine waking up to a stream of bookings and delighted guests, all while enjoying the financial rewards of the booming vacation rental market. Get a free airbnb clone business quotation.
https://appticz.com/airbnb-clone
localStorage.ampConfig = `{"amp":{"services":{"fitprofile":{"url":"http://127.0.0.1:5500/fitpredictor/demo/dist/loader.js","enabled":"true"},"fitprofilepreview":{"url":"http://127.0.0.1:5500/fitpredictor/demo/dist/fitProfilePreviewLoader.js","enabled":"true"}}}}`
.parent-container{
  //defining a grid container
  display: grid;
  //defining rows and columns
  grid-template: 50px 50px / 50px 50px;
  //defining a row and column gap
  gap: 20px 50px;
  
  
}

When we use the grid-template propertie, we are explicitly defining grid tracks to lay out our grid items. But when the grid needs more tracks for extra content, it will implicitly define new grid tracks. Additionally, the size values established from our grid-template propertie are not carried over into these implicit grid tracks. But we can define values for the implicit grid tracks.

Let’s say we want any new rows to stay the same value as our explicit row track sizes:

.parent-container {
  display: grid;
  grid-template: 50px 50px;
  grid-auto-rows: 50px;
}
<tts service="android" speed="1.0" voice="en-US" style="display: none">{{En}}</tts>
import React, { useState } from 'react';

const App = () => {
  // Initialize with one set of input boxes
  const [inputSets, setInputSets] = useState([{ id: Date.now() }]);

  const handleAddClick = () => {
    // Add a new set of input boxes
    setInputSets([...inputSets, { id: Date.now() }]);
  };

  const handleDeleteClick = (id) => {
    // Ensure at least one set of input boxes remains
    if (inputSets.length > 1) {
      setInputSets(inputSets.filter(set => set.id !== id));
    }
  };

  return (
    <div>
      <button onClick={handleAddClick}>
        Add Input Boxes
      </button>

      {inputSets.map(set => (
        <div key={set.id} style={{ marginBottom: '10px' }}>
          <input type="text" placeholder="Input 1" />
          <input type="text" placeholder="Input 2" />
          {/* Conditionally render the delete button */}
          {inputSets.length > 1 && (
            <button 
              onClick={() => handleDeleteClick(set.id)} 
              style={{ marginLeft: '10px' }}
            >
              Delete
            </button>
          )}
        </div>
      ))}
    </div>
  );
};

export default App;
document.addEventListener("DOMContentLoaded", function() {
  const addIcon = document.getElementById("add-icon");
  const dropdownMenu = document.getElementById("dropdown-menu");
  const wordInput = document.getElementById("word-input");
  const numWordsInput = document.getElementById("num-words-input");
  const generateButton = document.getElementById("generate-button");
  const resultElement = document.getElementById("word-list");
  const resetButton = document.getElementById("reset");
  const saveButton = document.createElement("button");
  saveButton.id = "save-button";
  saveButton.textContent = "Save";
  resetButton.insertAdjacentElement("afterend", saveButton);
  const modal = document.getElementById("listModal");
  const modalBody = document.getElementById("modal-body");
  const createListButton = document.getElementById("create-list");
  const viewListButton = document.getElementById("view-list");
  const closeModalButton = document.getElementById("close-modal");
  const deleteAllButton = document.createElement("button");
  deleteAllButton.id = "delete-all";
  deleteAllButton.textContent = "Delete All";
  closeModalButton.insertAdjacentElement("afterend", deleteAllButton);

  // Store lists in local storage
  let lists = JSON.parse(localStorage.getItem('lists')) || [];
  let definitionMap = {};

  // Toggle dropdown menu visibility
  addIcon.addEventListener("click", function() {
      dropdownMenu.style.display = dropdownMenu.style.display === "block" ? "none" : "block";
  });

  // Hide the dropdown menu if the user clicks outside of it
  document.addEventListener("click", function(event) {
      if (!addIcon.contains(event.target) && !dropdownMenu.contains(event.target)) {
          dropdownMenu.style.display = "none";
      }
  });

  // Create List button click handler
  createListButton.addEventListener("click", function() {
      openCreateListModal();
  });

  // View List button click handler
  viewListButton.addEventListener("click", function() {
      openViewListModal();
  });

  // Close modal when the user clicks on Close button
  closeModalButton.addEventListener("click", function() {
      modal.style.display = "none";
  });

  // Close modal when the user clicks outside of the modal
  window.addEventListener("click", function(event) {
      if (event.target === modal) {
          modal.style.display = "none";
      }
  });

  // Create Flip Card
function createFlipCard(word) {
  const card = document.createElement('li');
  card.classList.add('flip-container');
  card.innerHTML = `
    <div class="flip-card">
      <div class="front">${word}</div>
      <div class="back">${definitionMap[word] || 'Definition not found'}</div>
    </div>
  `;

  card.addEventListener('click', () => {
    card.querySelector('.flip-card').classList.toggle('flipped');
  });

  return card;
}


  // Generate random words with definitions
  async function generateRandomWords() {
      resultElement.classList.remove("error");

      const words = wordInput.value.split(",").map(word => word.trim()).filter(word => word !== "");
      if (words.length === 0) {
          resultElement.classList.add("error");
          resultElement.innerHTML = "Please enter some words separated by commas.";
          return;
      }

      const numWords = Math.min(numWordsInput.value, words.length);
      let selectedWords = getRandomWords(words, numWords);

      resultElement.innerHTML = ''; // Clear previous results
      for (const word of selectedWords) {
          const definition = await fetchDefinition(word);
          definitionMap[word] = definition;
          resultElement.appendChild(createFlipCard(word));
      }
      updateWordCounter();
  }

  // Get random words from the list
  function getRandomWords(words, numWords) {
      const shuffled = words.sort(() => 0.5 - Math.random());
      return shuffled.slice(0, numWords);
  }

  // Fetch word definition from API
  async function fetchDefinition(word) {
      try {
          const response = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`);
          if (!response.ok) {
              if (response.status === 404) {
                  return "Definition not found.";
              }
              throw new Error("Network response was not ok.");
          }
          const data = await response.json();
          if (data && data[0] && data[0].meanings && data[0].meanings[0] && data[0].meanings[0].definitions && data[0].meanings[0].definitions[0]) {
              return data[0].meanings[0].definitions[0].definition;
          } else {
              return "Definition not found.";
          }
      } catch (error) {
          console.error("Error fetching definition:", error);
          return "Definition not found.";
      }
  }

  // Attach event listeners to buttons
  generateButton.addEventListener("click", generateRandomWords);
  resetButton.addEventListener("click", function() {
      resultElement.innerHTML = "";
      wordInput.value = "";
      numWordsInput.value = 1;
      updateWordCounter();
  });

  // Save button click handler
  saveButton.addEventListener("click", function() {
      const words = wordInput.value.split(",").map(word => word.trim()).filter(word => word !== "");
      if (words.length === 0) {
          alert("Please enter some words to save.");
          return;
      }
      const listName = prompt("Enter the name of the list:");
      if (listName) {
          lists.push({ name: listName, words });
          localStorage.setItem('lists', JSON.stringify(lists));
          alert(`List "${listName}" saved.`);
      } else {
          alert("List name cannot be empty.");
      }
  });

  // Function to open create list modal
  function openCreateListModal() {
      // Clear previous content of modal body
      modalBody.innerHTML = "";

      // Create input field and submit button for entering list name
      const inputField = document.createElement("input");
      inputField.setAttribute("type", "text");
      inputField.setAttribute("placeholder", "Enter the name of the list");
      inputField.style.marginRight = "10px";

      const submitButton = document.createElement("button");
      submitButton.textContent = "Create";
      submitButton.addEventListener("click", function() {
          const listName = inputField.value.trim();
          if (listName) {
              lists.push({ name: listName, words: [] });
              localStorage.setItem('lists', JSON.stringify(lists));
              modal.style.display = "none"; // Hide modal after creating list
              alert(`List "${listName}" created.`);
              inputField.value = ""; // Clear input field
              openViewListModal(); // After creating, open view list modal
          } else {
              alert("Please enter a valid list name.");
          }
      });

      const cancelButton = document.createElement("button");
      cancelButton.textContent = "Cancel";
      cancelButton.addEventListener("click", function() {
          modal.style.display = "none";
      });

      modalBody.appendChild(inputField);
      modalBody.appendChild(submitButton);
      modalBody.appendChild(cancelButton);

      modal.style.display = "block"; // Display modal
      dropdownMenu.style.display = "none"; // Hide dropdown menu
  }

  // Function to open view list modal
  function openViewListModal() {
      if (lists.length === 0) {
          modalBody.innerHTML = "<p>No lists available.</p>";
      } else {
          modalBody.innerHTML = ""; // Clear previous content

          lists.forEach((list, index) => {
              const listItem = document.createElement("div");
              listItem.className = "list-item";
              listItem.textContent = list.name;

              // Add view, open, and delete buttons for each list item
              const viewButton = document.createElement("button");
              viewButton.textContent = "View";
              viewButton.addEventListener("click", function() {
                  openWordListModal(index);
              });

              const openButton = document.createElement("button");
              openButton.textContent = "Open";
              openButton.addEventListener("click", function() {
                  openList(index);
              });

              const deleteButton = document.createElement("button");
              deleteButton.textContent = "Delete";
              deleteButton.addEventListener("click", function() {
                  lists.splice(index, 1);
                  localStorage.setItem('lists', JSON.stringify(lists));
                  openViewListModal(); // Refresh view after deleting
              });

              listItem.appendChild(viewButton);
              listItem.appendChild(openButton);
              listItem.appendChild(deleteButton);
              modalBody.appendChild(listItem);
          });
      }

      modal.style.display = "block"; // Display modal
      dropdownMenu.style.display = "none"; // Hide dropdown menu
  }

  // Function to open word list modal
  function openWordListModal(listIndex) {
      // Clear previous content of modal body
      modalBody.innerHTML = "";

      // Display list name
      const listName = document.createElement("h2");
      listName.textContent = lists[listIndex].name;
      modalBody.appendChild(listName);

      // Display words
      const wordList = document.createElement("ul");
      lists[listIndex].words.forEach(word => {
          const wordItem = document.createElement("li");
          wordItem.textContent = word;
          wordList.appendChild(wordItem);
      });
      modalBody.appendChild(wordList);

      // Add input field and button for adding words to the list
      const wordInputField = document.createElement("input");
      wordInputField.setAttribute("type", "text");
      wordInputField.setAttribute("placeholder", "Enter words separated by commas");
      wordInputField.style.marginRight = "10px";

      const addButton = document.createElement("button");
      addButton.textContent = "Add";
      addButton.addEventListener("click", function() {
          const words = wordInputField.value.split(",").map(word => word.trim());
          lists[listIndex].words.push(...words);
          localStorage.setItem('lists', JSON.stringify(lists));
          openWordListModal(listIndex); // Refresh view after adding words
      });

      const closeButton = document.createElement("button");
      closeButton.textContent = "Close";
      closeButton.addEventListener("click", function() {
          modal.style.display = "none";
      });

      modalBody.appendChild(wordInputField);
      modalBody.appendChild(addButton);
      modalBody.appendChild(closeButton);

      modal.style.display = "block"; // Display modal
  }

  // Function to open list and populate the main search bar
  function openList(listIndex) {
      wordInput.value = lists[listIndex].words.join(", ");
      modal.style.display = "none"; // Hide modal
  }

  // Delete all button click handler
  deleteAllButton.addEventListener("click", function() {
      if (confirm("Are you sure you want to delete all lists?")) {
          lists = [];
          localStorage.setItem('lists', JSON.stringify(lists));
          openViewListModal(); // Refresh view after deleting all lists
      }
  });

  // Function to update word counter
  function updateWordCounter() {
      const wordCount = wordInput.value.split(",").filter(word => word.trim() !== "").length;
      document.getElementById("word-counter").textContent = `${wordCount} words`;
  }

  // Event listener to update word counter when input changes
  wordInput.addEventListener("input", updateWordCounter);

  // Initial call to update word counter on page load
  updateWordCounter();
});
ANIMATION_TIMEOUT = parseInt(getComputedStyle(rootEl).getPropertyValue('--transition-duration'), 10);
// 1. push()
// Appends one or more elements to the end of an array
let arr1 = [1, 2, 3];
arr1.push(4);
console.log(arr1); // [1, 2, 3, 4]

// 2. pop()
// Removes the last element from the array and returns it
let arr2 = [1, 2, 3];
let lastElement = arr2.pop();
console.log(lastElement); // 3
console.log(arr2); // [1, 2]

// 3. shift()
// Removes the first element from the array and returns it
let arr3 = [1, 2, 3];
let firstElement = arr3.shift();
console.log(firstElement); // 1
console.log(arr3); // [2, 3]

// 4. unshift()
// Appends one or more elements to the beginning of an array
let arr4 = [2, 3];
arr4.unshift(1);
console.log(arr4); // [1, 2, 3]

// 5. concat()
// Combines two or more arrays
let arr51 = [1, 2];
let arr52 = [3, 4];
let newArr5 = arr51.concat(arr52);
console.log(newArr5); // [1, 2, 3, 4]


// 6. slice()
// Returns a new array containing part of the original array
let arr6 = [1, 2, 3, 4];
let newArr6 = arr6.slice(1, 3);
console.log(newArr6); // [2, 3]

// 7. splice()
// Changes the contents of an array by removing, replacing, or adding new elements
let arr7 = [1, 2, 3, 4];
arr7.splice(1, 2, 'a', 'b');
console.log(arr7); // [1, 'a', 'b', 4]

// 8. forEach()
// Executes the specified function once for each element of the array
let arr8 = [1, 2, 3];
arr8.forEach(element => console.log(element));
// 1
// 2
// 3

// 9. map()
// Creates a new array with the results of calling the specified function for each element of the array
let arr9 = [1, 2, 3];
let newArr9 = arr9.map(element => element * 2);
console.log(newArr9); // [2, 4, 6]

// 10. filter()
// Creates a new array with all elements that passed the test implemented by the given function
let arr10 = [1, 2, 3, 4];
let newArr10 = arr10.filter(element => element % 2 === 0);
console.log(newArr10); // [2, 4]

// 11. reduce()
// Applies a function to each element of an array (from left to right) to reduce it to a single value
let arr11 = [1, 2, 3, 4];
let sum = arr11.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // 10

// 12. find()
// Returns the first element of the array that satisfies the given function
let arr12 = [1, 2, 3, 4];
let found = arr12.find(element => element > 2);
console.log(found); // 3

// 13. findIndex()
// Returns the index of the first array element that satisfies the given function
let arr13 = [1, 2, 3, 4];
let index = arr13.findIndex(element => element > 2);
console.log(index); // 2

// 14. some()
// Checks whether at least one element of the array satisfies the condition implemented by the function
let arr14 = [1, 2, 3, 4];
let hasEven = arr14.some(element => element % 2 === 0);
console.log(hasEven); // true

// 15. every()
// Checks whether all elements of an array satisfy the condition implemented by the function
let arr15 = [1, 2, 3, 4];
let allEven = arr15.every(element => element % 2 === 0);
console.log(allEven); // false

// 16. sort()
// Sorts the elements of an array and returns the sorted array
let arr16 = [3, 1, 4, 2];
arr16.sort();
console.log(arr16); // [1, 2, 3, 4]

// 17. reverse()
// Reverses the order of the elements in the array
let arr17 = [1, 2, 3, 4];
arr17.reverse();
console.log(arr17); // [4, 3, 2, 1]

// 18. join()
// Combines all elements of the array into a row
let arr18 = [1, 2, 3, 4];
let str = arr18.join('-');
console.log(str); // "1-2-3-4"

// 19. includes()
// Tests whether an array contains a specified element
let arr19 = [1, 2, 3, 4];
let hasThree = arr19.includes(3);
console.log(hasThree); // true

// 20. flat()
// Creates a new array with all subarray elements submerged to the specified depth
let arr20 = [1, [2, [3, [4]]]];
let flatArr = arr20.flat(2);
console.log(flatArr); // [1, 2, 3, [4]]

// 21. flatMap()
// First, it displays each element using a function, and then sums the result into a new array
let arr21 = [1, 2, 3];
let flatMappedArr = arr21.flatMap(element => [element, element * 2]);
console.log(flatMappedArr); // [1, 2, 2, 4, 3, 6]
import classnames from 'classnames';
import _ from 'lodash';
import React, {
  useCallback, useEffect, useRef, useState,
} from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import TTLCache from '@isaacs/ttlcache';
import SelectionAreaWrapper from '../../../../../ComponentsV2/SelectionAreaWrapper';
import { EMITTER_CONSTANTS } from '../../../../../ConstantsV2';
import {
  KEY_CANCEL, KEY_RECOIL, STRIP_BEAT_THUMBNAIL_INFO,
} from '../../../../../ConstantsV2/aiConstants';
import storeTimeout from '../../../../../Store';
import emitter from '../../../../../UtilsV2/eventEmitter';
import { toastrError } from '../../../../../UtilsV2/toastNotification';
import {
  activeTabState, beatChangesState, beatOptionsState, channelsThumbnailState, gainThumbnailState, highPassThumbnailState, isLoadingStripState, limitUpdateBeatsModalState, pageIndexState, resetAppliedBulkChangesState, rrHeatMapCellChangesState, selectedMultipleStripState, selectedStripState, sizeBeatThumbnailConfigState,
} from '../../Recoil';
import BeatPreviewStrip from './beatPreviewStrip';
import {
  ID_DIV_STRIP_BEATS,
  formatBeatStripRr, getListBeatTime,
} from '../helper';
import {
  bulkActionState,
  listSelectedCellsState, loadingBeatStatusState, totalStripState,
} from '../recoil';
import {
  BEAT_HR_TAB_ID, getStripIndex, getStripPosition, logError,
} from '../../handler';
import { useGetRecoilValue, useUpdateEffect } from '../../../../../UtilsV2/customHooks';
import fetchHolterRrCellsBeats from '../../../../../Apollo/Functions/fetchHolterRrCellsBeats';
import { BULK_ACTION_ENUM, LOAD_BEAT_STATUS } from '../recoil/model';
import selectCellHeatmapIcon from '../../../../../StaticV2/Images/Components/select-cell-heatmap-icon.svg';
import { checkTotalBeatsUpdate, LIMIT_BEATS_UPDATE, updateRrHeatMapCell } from './handler';

// Initialize cache with a time-to-live of 30 minutes
const beatsCache = new TTLCache({ ttl: 30 * 60 * 1000 }); 
const beatPreviewStripOffset = 2000;

const StripBeats = (props) => {
  const keyRecoil = KEY_RECOIL.TAB_1;
  const [isLoadingStrip, setIsLoadingStrip] = useRecoilState(isLoadingStripState(keyRecoil));
  const [selectedStrip, setSelectedStrip] = useRecoilState(selectedStripState(keyRecoil));
  const [loadingBeatStatus, setLoadingBeatStatus] = useRecoilState(loadingBeatStatusState);
  const activeButton = useRecoilValue(activeTabState(keyRecoil));
  const getActiveButton = useGetRecoilValue(activeTabState(keyRecoil));
  const pageIndex = useRecoilValue(pageIndexState(keyRecoil));
  const sizeThumbnailConfig = useRecoilValue(sizeBeatThumbnailConfigState);
  const channelsThumbnail = useRecoilValue(channelsThumbnailState);
  const gainThumbnail = useRecoilValue(gainThumbnailState);
  const highPassThumbnail = useRecoilValue(highPassThumbnailState);
  const bulkAction = useRecoilValue(bulkActionState);
  const listSelectedCells = useRecoilValue(listSelectedCellsState);
  const setSelectedMultipleStrip = useSetRecoilState(selectedMultipleStripState(keyRecoil));
  const setTotalStrip = useSetRecoilState(totalStripState);
  const getListSelectedCells = useGetRecoilValue(listSelectedCellsState);
  const setPageIndex = useSetRecoilState(pageIndexState(keyRecoil));
  const getBeatChanges = useGetRecoilValue(beatChangesState(keyRecoil));
  const setBeatChanges = useSetRecoilState(beatChangesState(keyRecoil));
  const getRrHeatMapCellChanges = useGetRecoilValue(rrHeatMapCellChangesState);
  const setRrHeatMapCellChanges = useSetRecoilState(rrHeatMapCellChangesState);
  const getResetAppliedBulkChanges = useGetRecoilValue(resetAppliedBulkChangesState);
  const setResetAppliedBulkChanges = useSetRecoilState(resetAppliedBulkChangesState);
  const setLimitUpdateBeatsModalState = useSetRecoilState(limitUpdateBeatsModalState);
  const setBeatOptions = useSetRecoilState(beatOptionsState(keyRecoil));

  const stripPosition = useRef({ x: 0, y: 0 });
  const prevPageIndex = useRef(pageIndex.index);
  const lastCurrentCellsHighestPage = useRef(0);
  const arrowKeyToPrevPage = useRef(false);
  const selectionAreaWrapperRef = useRef();
  const timeoutRef = useRef();
  const cache = useRef(new Map()); // Create a cache
  const [holterBeats, setHolterBeats] = useState([]);
  const [isFetchingAllData, setIsFetchingAllData] = useState(false);
  let isActiveQuery;

  // Handle drag select
  const setSelectedBeat = useCallback((selected) => {
    //* Convert Set to array
    const selectedArr = [...selected];
    const selectedBeats = [];
    _.forEach(selectedArr, (item) => {
      const id = item.split('-')[1];
      const selectedBeat = (holterBeats || []).find(beat => beat.id === Number(id));
      if (selectedBeat) {
        selectedBeats.push(selectedBeat);
      }
    });
    setSelectedMultipleStrip((prev) => {
      if (prev.length === 0 && selectedBeats.length === 0) {
        return prev;
      }
      return selectedBeats;
    });
  }, [holterBeats]);

  // Fetch background data for caching
  const fetchBackgroundData = async (rrCellIds, totalBeats) => {
    setIsFetchingAllData(true);
    try {
      const timerId = setTimeout(async () => {
        storeTimeout.removeStore(activeButton, timerId);
        const filter = {
          studyId: props.studyId,
          profileId: props.profileId,
          rrHeatMapType: activeButton,
          rrCellIds,
        };
        const {
          beats,
          hesBeatStatus,
          beatChannels,
          beatHeatMapIds,
        } = await fetchHolterRrCellsBeats(filter, totalBeats, KEY_CANCEL.API_HOLTER_AI);
        beatsCache.set('fetchCacheHolterBeats', {
          beats, hesBeatStatus, beatChannels, beatHeatMapIds,
        });
        // setIsFetchingAllData(false);
      }, 0);
      timeoutRef.current = timerId;
      storeTimeout.pushStore(activeButton, timerId);
    }
    catch (err) {
      logError(err);
    }
  };

  // Get a slice of cached data
  const getCachedDataSlice = (data, startIndex, endIndex) => ({
    beats: data.beats.slice(startIndex, endIndex),
    hesBeatStatus: data.hesBeatStatus.slice(startIndex, endIndex),
    beatChannels: data.beatChannels.slice(startIndex, endIndex),
    beatHeatMapIds: data.beatHeatMapIds.slice(startIndex, endIndex),
  });

  // fech background more beat rr when load all not done
  const fetchBackgroundMoreBeatRr = (index, rrCellIds, waitTime = 0, stopCall = false) => {
    if (cache.current.has(index)) {
      return Promise.resolve();
    }
    cache.current.set(index, true);
    const timerId = setTimeout(async () => {
      storeTimeout.removeStore(activeButton, timerId);
      const filter = {
        studyId: props.studyId,
        profileId: props.profileId,
        rrHeatMapType: activeButton,
        rrCellIds,
        skip: index * STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit,
      };
      await fetchHolterRrCellsBeats(filter, STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit, KEY_CANCEL.API_HOLTER_AI);
    }, waitTime);
    timeoutRef.current = timerId;
    storeTimeout.pushStore(activeButton, timerId);
  };

  // Fetch beats data and handle caching
  const fetchBeats = async ({ rrCellIds }) => {
    setIsLoadingStrip(true);
    try {
      const validatePage = pageIndex.index <= 0 ? 0 : pageIndex.index;
      const filter = {
        studyId: props.studyId,
        profileId: props.profileId,
        rrHeatMapType: activeButton,
        rrCellIds,
        skip: validatePage * STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit,
      };

      let data = [];
      let listDate = [];

      if (beatsCache.has('fetchCacheHolterBeats')) {
        const startIndex = validatePage * STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit;
        const endIndex = (validatePage + 1) * STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit;
        const slicedData = getCachedDataSlice(beatsCache.get('fetchCacheHolterBeats'), startIndex, endIndex);
        data = formatBeatStripRr(slicedData.beats, slicedData.hesBeatStatus, slicedData.beatChannels, slicedData.beatHeatMapIds);
        listDate = getListBeatTime(slicedData.beats, props.timezoneOffset);
      } else {
        const {
          beats, hesBeatStatus, beatChannels, totalBeats, beatHeatMapIds,
        } = await fetchHolterRrCellsBeats(filter, STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit, KEY_CANCEL.API_HOLTER_AI);
        data = formatBeatStripRr(beats, hesBeatStatus, beatChannels, beatHeatMapIds);
        listDate = getListBeatTime(beats, props.timezoneOffset);
        const pageLoad = validatePage + 1;
        if (totalBeats > pageLoad * STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit) {
          const maxPageLoad = Math.ceil(totalBeats / STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit);
          // fetch background 3 pages
          for (let i = pageLoad; i < Math.min(pageLoad + 3, maxPageLoad); i += 1) {
            fetchBackgroundMoreBeatRr(i, rrCellIds);
          }
        }
        if(!isFetchingAllData) {
          fetchBackgroundData(rrCellIds, totalBeats);
        }
        setTotalStrip(totalBeats ?? 0);
      }

      if (isActiveQuery) {
        props.callbackListDate(listDate);
        if (validatePage < prevPageIndex.current && arrowKeyToPrevPage.current) {
          stripPosition.current = getStripPosition(data.length - 1, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          setSelectedStrip(data[data.length - 1]);
        } else {
          const selectedStripIndex = _.findIndex(data, x => x.id === selectedStrip?.id);
          const tempSelectedStrip = selectedStripIndex === -1 ? (data[0] || null) : data[selectedStripIndex];
          stripPosition.current = getStripPosition(selectedStripIndex === -1 ? 0 : selectedStripIndex, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          setSelectedStrip(tempSelectedStrip);
        }
        arrowKeyToPrevPage.current = false;
        prevPageIndex.current = validatePage;
        setHolterBeats(data);
        setIsLoadingStrip(false);
      }
    } catch (error) {
      logError('Failed to fetch rr heatmap beats: ', error);
      toastrError(error.message, 'Error');
    }
  };

  // Handle beat selection on click
  const handleClickSelectBeat = useCallback((beat, index) => {
    setSelectedStrip((prev) => {
      if (beat?.id !== prev?.id) {
        stripPosition.current = getStripPosition(index, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
        return beat;
      }
      return prev;
    });
  }, []);

  // Update page index when size of thumbnail config changes
  useUpdateEffect(() => {
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.FETCH) {
      const { x: row, y: column } = stripPosition.current;
      const currentStripIndexInList = column + row * STRIP_BEAT_THUMBNAIL_INFO.prevStripPerRow;
      const currentBeatIndex = currentStripIndexInList + pageIndex.index * STRIP_BEAT_THUMBNAIL_INFO.prevStripDisplayLimit;
      const newPage = Math.floor(currentBeatIndex / STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit);
      setPageIndex({ index: newPage < 0 ? 0 : newPage });
    }
  }, [sizeThumbnailConfig]);

  // Handle various loading statuses
  useEffect(() => {
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.NONE) {
      setIsLoadingStrip(false);
      setHolterBeats([]);
    }
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.FETCH) {
      isActiveQuery = true;
      const listSelectedCells = _.filter(getListSelectedCells(), x => !_.isNaN(Number(x.id)));
      const rrCellIds = _.map(listSelectedCells, x => x.id);
      if (rrCellIds.length !== 0) {
        fetchBeats({ rrCellIds });
      }
    }
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.RELOAD) {
      isActiveQuery = true;
      const listSelectedCells = _.filter(getListSelectedCells(), x => !_.isNaN(Number(x.id)));
      const rrCellIds = _.map(listSelectedCells, x => x.id);
      if (rrCellIds.length !== 0 && selectedStrip?.id) {
        setLoadingBeatStatus({ status: LOAD_BEAT_STATUS.FETCH });
        setPageIndex({ index: 0 });
      }
    }
    return () => {
      isActiveQuery = false;
    };
  }, [loadingBeatStatus, pageIndex]);

  // Reset last highest page when selected cells change
  useEffect(() => {
    lastCurrentCellsHighestPage.current = 0;
    cache.current.clear(); // Clear cache when listSelectedCells changes
  }, [listSelectedCells]);

  // Handle bulk action updates
  useUpdateEffect(() => {
    if (!_.isEmpty(bulkAction) && bulkAction?.[0] !== BULK_ACTION_ENUM.NOTHING && !isLoadingStrip) {
      const listSelectedCells = _.filter(getListSelectedCells(), x => !_.isNaN(Number(x.id)));
      if (!_.isEmpty(listSelectedCells)) {
        const totalBeatUpdating = checkTotalBeatsUpdate({
          currentCells: getRrHeatMapCellChanges(),
          newCells: listSelectedCells,
          resetAppliedBeats: getResetAppliedBulkChanges(),
          beatChanges: getBeatChanges(),
        });
        if (totalBeatUpdating > LIMIT_BEATS_UPDATE) {
          setLimitUpdateBeatsModalState({
            isShowModal: true,
            totalBeatUpdating,
          });
        } else {
          const activeButton = getActiveButton();
          const { rrHeatMapChanges, resetAppliedBeats, newBeatsChange } = updateRrHeatMapCell({
            updatedCells: getRrHeatMapCellChanges(),
            newCells: listSelectedCells,
            newType: bulkAction?.[0],
            activeButton,
            resetAppliedBeats: getResetAppliedBulkChanges(),
            beatChanges: getBeatChanges(),
          });
          setResetAppliedBulkChanges(resetAppliedBeats);
          setRrHeatMapCellChanges(rrHeatMapChanges);
          setBeatChanges(newBeatsChange);
          setBeatOptions({
            beatChangesState: newBeatsChange,
            resetAppliedBulkChangesState: resetAppliedBeats,
            rrHeatMapCellChangesState: rrHeatMapChanges,
          });
        }
      }
    }
  }, [bulkAction]);

  // Handle keyboard navigation
  useEffect(() => {
    const handleKeyDown = (event) => {
      if (_.isEmpty(selectionAreaWrapperRef.current) || event?.repeat || isLoadingStrip) {
        return;
      }
      const tabZone = document.getElementById(BEAT_HR_TAB_ID);
      // *:Active key press when hover in tab zone
      if (tabZone && !tabZone.matches(':hover')) {
        return;
      }
      event.preventDefault();
      const { key } = event;
      switch (key) {
        case 'ArrowRight': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.y += 1;
          const index = getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          if (index > holterBeats.length - 1) {
            stripPosition.current.y -= 1;
            emitter.emit(EMITTER_CONSTANTS.BEAT_HR_NEXT);
          } else {
            const beat = holterBeats[index];
            if (beat) {
              setSelectedStrip(beat);
            } else {
              stripPosition.current.y -= 1;
            }
          }
          break;
        }
        case 'ArrowLeft': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.y -= 1;
          const index = getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          if (index < 0) {
            arrowKeyToPrevPage.current = true;
            stripPosition.current.y += 1;
            emitter.emit(EMITTER_CONSTANTS.BEAT_HR_PREV);
          } else {
            const beat = holterBeats[index];
            if (beat) {
              setSelectedStrip(beat);
            } else {
              stripPosition.current.y += 1;
            }
          }
          break;
        }
        case 'ArrowUp': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.x -= 1;
          const beat = holterBeats[getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow)];
          if (beat) {
            setSelectedStrip(beat);
          } else {
            stripPosition.current.x += 1;
          }
          break;
        }
        case 'ArrowDown': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.x += 1;
          const beat = holterBeats[getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow)];
          if (beat) {
            setSelectedStrip(beat);
          } else {
            stripPosition.current.x -= 1;
          }
          break;
        }
        default:
      }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [holterBeats]);

  // Clean up on component unmount
  useEffect(() => () => {
    storeTimeout.removeStore(activeButton, timeoutRef.current);
    beatsCache.clear();
    setIsFetchingAllData(false);
  }, [activeButton]);

  if (loadingBeatStatus.status === LOAD_BEAT_STATUS.NONE) {
    return (
      <div id={ID_DIV_STRIP_BEATS} className="no-cell-selected">
        <img src={selectCellHeatmapIcon} alt="select cell on heat map" />
        Select a cell on the heatmap chart below to view its beats.
      </div>
    );
  }

  return (
    <SelectionAreaWrapper
      ref={selectionAreaWrapperRef}
      id={ID_DIV_STRIP_BEATS}
      className="strips-container"
      selectables=".single-strip-beat"
      handleSetSelected={setSelectedBeat}
      defaultSelectedId={selectedStrip?.id}
      isNoData={holterBeats?.length === 0}
      isLoading={isLoadingStrip}
    >
      {
        selected => (
          <div className={classnames('strips-img-container')}>
            {
              _.map(holterBeats, (beat, index) => (
                <BeatPreviewStrip
                  key={beat.id}
                  index={index}
                  id={`beat-${beat.id}`}
                  isActive={beat?.id === selectedStrip?.id}
                  isSelected={selected.has(`beat-${beat.id}`)}
                  beat={beat}
                  gain={gainThumbnail}
                  channels={channelsThumbnail}
                  width={sizeThumbnailConfig.width}
                  height={sizeThumbnailConfig.height}
                  ecgDataMap={props.ecgDataMap}
                  highPass={highPassThumbnail}
                  offset={beatPreviewStripOffset}
                  studyId={props.studyId}
                  profileId={props.profileId}
                  onClickSelectBeat={handleClickSelectBeat}
                  activeButton={activeButton}
                />
              ))
            }
          </div>
        )
      }
    </SelectionAreaWrapper>
  );
};

export default StripBeats;
javascript: (function () {
  let host = location.host;
  if (host.includes("youtube.com")) {
    const data = JSON.parse(
      document.getElementsByClassName("PlayerMicroformatRendererHost")[0]
        .textContent,
    );
    window.location.assign(data["embedUrl"]);
  } else {
    window.alert("not YouTube");
  }
})();
import classnames from 'classnames';
import _ from 'lodash';
import React, {
  useCallback, useEffect, useMemo, useRef, useState,
} from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { c, t } from 'ttag';
import fetchHolterRrCellsBeats from '../../../../../Apollo/Functions/fetchHolterRrCellsBeats';
import SelectionAreaWrapper from '../../../../../ComponentsV2/SelectionAreaWrapper';
import { EMITTER_CONSTANTS } from '../../../../../ConstantsV2';
import {
  KEY_CANCEL, KEY_RECOIL, STRIP_BEAT_THUMBNAIL_INFO,
} from '../../../../../ConstantsV2/aiConstants';
import selectCellHeatmapIcon from '../../../../../StaticV2/Images/Components/select-cell-heatmap-icon.svg';
import storeTimeout from '../../../../../Store';
import { useGetRecoilValue, useUpdateEffect } from '../../../../../UtilsV2/customHooks';
import emitter from '../../../../../UtilsV2/eventEmitter';
import { toastrError } from '../../../../../UtilsV2/toastNotification';
import { checkAndProcessEcgData } from '../../common/handlerEcgData';
import {
  BEAT_HR_TAB_ID, getStripIndex, getStripPosition, logError,
} from '../../handler';
import {
  activeTabState, beatChangesState, beatOptionsState, channelsThumbnailState, gainThumbnailState, highPassThumbnailState, isLoadingStripState, limitUpdateBeatsModalState, pageIndexState, resetAppliedBulkChangesState, rrHeatMapCellChangesState, selectedMultipleStripState, selectedStripState, sizeBeatThumbnailConfigState,
} from '../../Recoil';
import {
  formatBeatStripRr, getListBeatTime,
  ID_DIV_STRIP_BEATS,
} from '../helper';
import {
  bulkActionState,
  listSelectedCellsState, loadingBeatStatusState, totalStripState,
} from '../recoil';
import { BULK_ACTION_ENUM, LOAD_BEAT_STATUS } from '../recoil/model';
import BeatPreviewStrip from './beatPreviewStrip';
import { checkTotalBeatsUpdate, LIMIT_BEATS_UPDATE, updateRrHeatMapCell } from './handler';

const beatPreviewStripOffset = 2000;

const StripBeats = (props) => {
  const keyRecoil = KEY_RECOIL.TAB_1;
  const [isLoadingStrip, setIsLoadingStrip] = useRecoilState(isLoadingStripState(keyRecoil));
  const [selectedStrip, setSelectedStrip] = useRecoilState(selectedStripState(keyRecoil));
  const [loadingBeatStatus, setLoadingBeatStatus] = useRecoilState(loadingBeatStatusState);
  const activeButton = useRecoilValue(activeTabState(keyRecoil));
  const getActiveButton = useGetRecoilValue(activeTabState(keyRecoil));
  const pageIndex = useRecoilValue(pageIndexState(keyRecoil));
  const sizeThumbnailConfig = useRecoilValue(sizeBeatThumbnailConfigState);
  const channelsThumbnail = useRecoilValue(channelsThumbnailState);
  const gainThumbnail = useRecoilValue(gainThumbnailState);
  const highPassThumbnail = useRecoilValue(highPassThumbnailState);
  const bulkAction = useRecoilValue(bulkActionState);
  const listSelectedCells = useRecoilValue(listSelectedCellsState);
  const setSelectedMultipleStrip = useSetRecoilState(selectedMultipleStripState(keyRecoil));
  const setTotalStrip = useSetRecoilState(totalStripState);
  const getListSelectedCells = useGetRecoilValue(listSelectedCellsState);
  const setPageIndex = useSetRecoilState(pageIndexState(keyRecoil));
  const getBeatChanges = useGetRecoilValue(beatChangesState(keyRecoil));
  const setBeatChanges = useSetRecoilState(beatChangesState(keyRecoil));
  const getRrHeatMapCellChanges = useGetRecoilValue(rrHeatMapCellChangesState);
  const setRrHeatMapCellChanges = useSetRecoilState(rrHeatMapCellChangesState);
  const getResetAppliedBulkChanges = useGetRecoilValue(resetAppliedBulkChangesState);
  const setResetAppliedBulkChanges = useSetRecoilState(resetAppliedBulkChangesState);
  const setLimitUpdateBeatsModalState = useSetRecoilState(limitUpdateBeatsModalState);
  const setBeatOptions = useSetRecoilState(beatOptionsState(keyRecoil));

  const stripPosition = useRef({ x: 0, y: 0 });
  const prevPageIndex = useRef(pageIndex.index);
  const lastCurrentCellsHighestPage = useRef(0);
  const arrowKeyToPrevPage = useRef(false);
  const selectionAreaWrapperRef = useRef();
  const timeoutRef = useRef();
  const cache = useRef(new Map()); // Create a cache

  const [holterBeats, setHolterBeats] = useState([]);
  const [rawHolterBeats, setRawHolterBeats] = useState([]);
  const [prevPageNumber, setPrevPageNumber] = useState(pageIndex.index); // Thêm state cho prevPageNumber
  let isActiveQuery;

  const pageToFetch = 5;
  const pageToFetchMore = 2;
  const stripDisplayLimitFetch = STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit * pageToFetch;

  const slicedHolterBeats = useMemo(() => {
    const startSlice = (pageIndex.index * STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit) % stripDisplayLimitFetch;
    const endSlice = startSlice + STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit;
    return holterBeats.slice(startSlice, endSlice);
  }, [pageIndex.index, holterBeats]);

  // Handle drag select
  const setSelectedBeat = useCallback((selected) => {
    const selectedArr = [...selected];
    const selectedBeats = [];
    _.forEach(selectedArr, (item) => {
      const id = item.split('-')[1];
      const selectedBeat = (holterBeats || []).find(beat => beat.id === Number(id));
      if (selectedBeat) {
        selectedBeats.push(selectedBeat);
      }
    });
    setSelectedMultipleStrip((prev) => {
      if (prev.length === 0 && selectedBeats.length === 0) {
        return prev;
      }
      return selectedBeats;
    });
  }, [holterBeats]);

  const fetchBackgroundMoreBeatRr = async (index, rrCellIds) => {
    if (cache.current.has(index)) {
      return Promise.resolve();
    }

    cache.current.set(index, true);

    const filter = {
      studyId: props.studyId,
      profileId: props.profileId,
      rrHeatMapType: activeButton,
      rrCellIds,
      skip: index * stripDisplayLimitFetch,
    };

    try {
      const {
        beats,
        hesBeatStatus,
        beatChannels,
        beatHeatMapIds,
      } = await fetchHolterRrCellsBeats(filter, stripDisplayLimitFetch, KEY_CANCEL.API_HOLTER_AI);
      const data = formatBeatStripRr(beats, hesBeatStatus, beatChannels, beatHeatMapIds);
      _.forEach(data, (d) => {
        checkAndProcessEcgData({
          ecgDataMap: props.ecgDataMap,
          beat: d,
          offset: beatPreviewStripOffset,
          dataLength: props.ecgDataMap.samplingFrequency * 2,
        });
      });
      cache.current.set(index, data); // Save data to cache
    } catch (error) {
      console.error(`Error fetching data for index ${index}:`, error);
    }
  };

  const fetchBeats = async ({ rrCellIds }) => {
    try {
      const validatePage = pageIndex.index <= 0 ? 0 : pageIndex.index;
      let data = [];
      let fetchedHolterBeats = [];

      if (validatePage === 0 || (((validatePage + 1) % pageToFetch) === 0)) {
        if (validatePage === 0) {
          setIsLoadingStrip(true);
        }
        if (validatePage > prevPageNumber + pageToFetchMore || validatePage < prevPageNumber - pageToFetchMore) {
          setIsLoadingStrip(true);
        }

        const pageLoad = validatePage / pageToFetch;
        const filter = {
          studyId: props.studyId,
          profileId: props.profileId,
          rrHeatMapType: activeButton,
          rrCellIds,
          skip: pageLoad * stripDisplayLimitFetch,
        };
        const {
          beats,
          hesBeatStatus,
          beatChannels,
          totalBeats,
          beatHeatMapIds,
        } = await fetchHolterRrCellsBeats(filter, stripDisplayLimitFetch, KEY_CANCEL.API_HOLTER_AI);
        fetchedHolterBeats = beats;

        const nextPageLoad = pageLoad + 1;
        if (totalBeats > nextPageLoad * stripDisplayLimitFetch) {
          const maxPageLoad = Math.ceil(totalBeats / STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit);
          const startFetchBackgroundIndex = cache.current.size + 1;
          for (let i = startFetchBackgroundIndex; i < Math.min(startFetchBackgroundIndex + pageToFetchMore, maxPageLoad); i += 1) {
            fetchBackgroundMoreBeatRr(i, rrCellIds);
          }
        }

        data = formatBeatStripRr(beats, hesBeatStatus, beatChannels, beatHeatMapIds);
        cache.current.set(pageLoad, data); // Save data to cache
        setIsLoadingStrip(false);
        if (isActiveQuery) {
          setTotalStrip(totalBeats ?? 0);
          setHolterBeats(data);
          setRawHolterBeats(fetchedHolterBeats);
        }
      } else {
        fetchedHolterBeats = rawHolterBeats;
        data = slicedHolterBeats;
      }

      const listDate = getListBeatTime(fetchedHolterBeats, props.timezoneOffset);

      if (isActiveQuery) {
        props.callbackListDate(listDate);
        //* Arrow button to prev page
        if (arrowKeyToPrevPage.current) {
          stripPosition.current = getStripPosition(data.length - 1, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          setSelectedStrip(data[data.length - 1]);
        } else {
          const selectedStripIndex = _.findIndex(data, x => x.id === selectedStrip?.id);
          const tempSelectedStrip = selectedStripIndex === -1 ? (data[0] || null) : data[selectedStripIndex];
          stripPosition.current = getStripPosition(selectedStripIndex === -1 ? 0 : selectedStripIndex, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          setSelectedStrip(tempSelectedStrip);
        }
        prevPageIndex.current = validatePage;
        if (validatePage === 0) {
          setIsLoadingStrip(false);
        }
      }
    } catch (error) {
      logError('Failed to fetch rr heatmap beats: ', error);
      toastrError(error.message, 'Error');
    }
  };

  const updateHolterBeats = (newPageIndex, isGoingBack) => {
    const pageLoad = Math.floor(newPageIndex / pageToFetch);
    const cachedData = cache.current.get(pageLoad);
    if (cachedData) {
      setHolterBeats(cachedData);
    } else {
      const listSelectedCells = _.filter(getListSelectedCells(), x => !_.isNaN(Number(x.id)));
      const rrCellIds = _.map(listSelectedCells, x => x.id);
      if (rrCellIds.length !== 0) {
        fetchBeats({ rrCellIds });
      }
    }
  };

  const handleClickSelectBeat = useCallback((beat, index) => {
    setSelectedStrip((prev) => {
      if (beat?.id !== prev?.id) {
        stripPosition.current = getStripPosition(index, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
        return beat;
      }
      return prev;
    });
  }, []);

  useEffect(() => {
    arrowKeyToPrevPage.current = false;
  }, []);

  useEffect(() => {
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.FETCH) {
      const { x: row, y: column } = stripPosition.current;
      const currentStripIndexInList = column + row * STRIP_BEAT_THUMBNAIL_INFO.prevStripPerRow;
      const currentBeatIndex = currentStripIndexInList + pageIndex.index * STRIP_BEAT_THUMBNAIL_INFO.prevStripDisplayLimit;
      const newPage = Math.floor(currentBeatIndex / STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit);
      setPageIndex({ index: newPage < 0 ? 0 : newPage });
    }
  }, [sizeThumbnailConfig]);

  useEffect(() => {
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.NONE) {
      setIsLoadingStrip(false);
      setHolterBeats([]);
    }
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.FETCH) {
      isActiveQuery = true;
      const listSelectedCells = _.filter(getListSelectedCells(), x => !_.isNaN(Number(x.id)));
      const rrCellIds = _.map(listSelectedCells, x => x.id);
      if (rrCellIds.length !== 0) {
        fetchBeats({ rrCellIds });
      }
    }
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.RELOAD) {
      isActiveQuery = true;
      const listSelectedCells = _.filter(getListSelectedCells(), x => !_.isNaN(Number(x.id)));
      const rrCellIds = _.map(listSelectedCells, x => x.id);
      if (rrCellIds.length !== 0 && selectedStrip?.id) {
        // reloadBeats(rrCellIds);
        setLoadingBeatStatus({ status: LOAD_BEAT_STATUS.FETCH });
        setPageIndex({ index: 0 });
      }
    }
    return () => {
      isActiveQuery = false;
    };
  }, [loadingBeatStatus, pageIndex]);

  useEffect(() => {
    lastCurrentCellsHighestPage.current = 0;
    cache.current.clear(); // Clear cache when listSelectedCells changes
  }, [listSelectedCells]);

  useEffect(() => {
    if (!_.isEmpty(bulkAction) && bulkAction?.[0] !== BULK_ACTION_ENUM.NOTHING && !isLoadingStrip) {
      const listSelectedCells = _.filter(getListSelectedCells(), x => !_.isNaN(Number(x.id)));
      if (!_.isEmpty(listSelectedCells)) {
        const totalBeatUpdating = checkTotalBeatsUpdate({
          currentCells: getRrHeatMapCellChanges(),
          newCells: listSelectedCells,
          resetAppliedBeats: getResetAppliedBulkChanges(),
          beatChanges: getBeatChanges(),
        });
        if (totalBeatUpdating > LIMIT_BEATS_UPDATE) {
          setLimitUpdateBeatsModalState({
            isShowModal: true,
            totalBeatUpdating,
          });
        } else {
          const activeButton = getActiveButton();
          const { rrHeatMapChanges, resetAppliedBeats, newBeatsChange } = updateRrHeatMapCell({
            updatedCells: getRrHeatMapCellChanges(),
            newCells: listSelectedCells,
            newType: bulkAction?.[0],
            activeButton,
            resetAppliedBeats: getResetAppliedBulkChanges(),
            beatChanges: getBeatChanges(),
          });
          setResetAppliedBulkChanges(resetAppliedBeats);
          setRrHeatMapCellChanges(rrHeatMapChanges);
          setBeatChanges(newBeatsChange);
          setBeatOptions({
            beatChangesState: newBeatsChange,
            resetAppliedBulkChangesState: resetAppliedBeats,
            rrHeatMapCellChangesState: rrHeatMapChanges,
          });
        }
      }
    }
  }, [bulkAction]);

  useEffect(() => {
    const handleKeyDown = (event) => {
      if (_.isEmpty(selectionAreaWrapperRef.current) || event?.repeat || isLoadingStrip) {
        return;
      }
      const tabZone = document.getElementById(BEAT_HR_TAB_ID);
      // *:Active key press when hover in tab zone
      if (tabZone && !tabZone.matches(':hover')) {
        return;
      }
      event.preventDefault();
      const { key } = event;
      switch (key) {
        case 'ArrowRight': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.y += 1;
          const index = getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          arrowKeyToPrevPage.current = false;
          if (index > slicedHolterBeats.length - 1) {
            stripPosition.current.y -= 1;
            emitter.emit(EMITTER_CONSTANTS.BEAT_HR_NEXT);
          } else {
            const beat = slicedHolterBeats[index];
            if (beat) {
              setSelectedStrip(beat);
            } else {
              stripPosition.current.y -= 1;
            }
          }
          break;
        }
        case 'ArrowLeft': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.y -= 1;
          const index = getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          if (index < 0) {
            stripPosition.current.y += 1;
            emitter.emit(EMITTER_CONSTANTS.BEAT_HR_PREV);
          } else {
            const beat = slicedHolterBeats[index];
            if (beat) {
              setSelectedStrip(beat);
            } else {
              stripPosition.current.y += 1;
            }
          }
          arrowKeyToPrevPage.current = true;
          break;
        }
        case 'ArrowUp': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.x -= 1;
          const beat = slicedHolterBeats[getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow)];
          if (beat) {
            setSelectedStrip(beat);
          } else {
            stripPosition.current.x += 1;
          }
          break;
        }
        case 'ArrowDown': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.x += 1;
          const beat = slicedHolterBeats[getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow)];
          if (beat) {
            setSelectedStrip(beat);
          } else {
            stripPosition.current.x -= 1;
          }
          break;
        }
        default:
      }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [slicedHolterBeats, isLoadingStrip]);

  useEffect(() => {
    if (selectedStrip) {
      const selectedBeat = slicedHolterBeats.find(beat => beat.id === selectedStrip.id);
      if (!selectedBeat && slicedHolterBeats.length > 0) {
        if (arrowKeyToPrevPage.current) {
          setSelectedStrip(slicedHolterBeats[slicedHolterBeats.length - 1]);
          stripPosition.current = getStripPosition(slicedHolterBeats.length - 1, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
        } else {
          setSelectedStrip(slicedHolterBeats[0]);
          stripPosition.current = getStripPosition(0, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
        }
      } else if (selectedBeat) {
        setSelectedStrip(selectedBeat);
        const index = slicedHolterBeats.indexOf(selectedBeat);
        stripPosition.current = getStripPosition(index, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
      }
    }
  }, [selectedStrip, slicedHolterBeats]);
  

  useEffect(() => () => {
    storeTimeout.removeStore(activeButton, timeoutRef.current);
  }, [activeButton]);

  // Update fetch logic to handle specific pageIndex changes
  useEffect(() => {
    if (isActiveQuery) {
      const listSelectedCells = _.filter(getListSelectedCells(), x => !_.isNaN(Number(x.id)));
      const rrCellIds = _.map(listSelectedCells, x => x.id);
      if (rrCellIds.length !== 0) {
        updateHolterBeats(pageIndex.index); // Update holter beats based on page index
      }
    }
    setPrevPageNumber(prevPageIndex.current); // Cập nhật prevPageNumber mỗi khi pageIndex thay đổi
  }, [pageIndex]);

  if (loadingBeatStatus.status === LOAD_BEAT_STATUS.NONE) {
    return (
      <div id={ID_DIV_STRIP_BEATS} className="no-cell-selected">
        <img src={selectCellHeatmapIcon} alt="select cell on heat map" />
        {t`Select a cell on the heatmap chart below to view its beats.`}
      </div>
    );
  }

  return (
    <SelectionAreaWrapper
      ref={selectionAreaWrapperRef}
      id={ID_DIV_STRIP_BEATS}
      className="strips-container"
      selectables=".single-strip-beat"
      handleSetSelected={setSelectedBeat}
      defaultSelectedId={selectedStrip?.id}
      isNoData={slicedHolterBeats?.length === 0}
      isLoading={isLoadingStrip}
    >
      {
        selected => (
          <div className={classnames('strips-img-container')}>
            {
              _.map(slicedHolterBeats, (beat, index) => (
                <BeatPreviewStrip
                  key={beat.id}
                  index={index}
                  id={`beat-${beat.id}`}
                  isActive={beat?.id === selectedStrip?.id}
                  isSelected={selected.has(`beat-${beat.id}`)}
                  beat={beat}
                  gain={gainThumbnail}
                  channels={channelsThumbnail}
                  width={sizeThumbnailConfig.width}
                  height={sizeThumbnailConfig.height}
                  ecgDataMap={props.ecgDataMap}
                  highPass={highPassThumbnail}
                  offset={beatPreviewStripOffset}
                  studyId={props.studyId}
                  profileId={props.profileId}
                  onClickSelectBeat={handleClickSelectBeat}
                  activeButton={activeButton}
                />
              ))
            }
          </div>
        )
      }
    </SelectionAreaWrapper>
  );
};

export default StripBeats;
The for...of loop
The basic tool for looping through a collection is the for...of loop:

js
Copy to Clipboard
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];

for (const cat of cats) {
  console.log(cat);
}
In this example, for (const cat of cats) says:

Given the collection cats, get the first item in the collection.
Assign it to the variable cat and then run the code between the curly braces {}.
Get the next item, and repeat (2) until you've reached the end of the collection.

-------------------------------------------------
  
map() and filter()
JavaScript also has more specialized loops for collections, and we'll mention two of them here.

You can use map() to do something to each item in a collection and create a new collection containing the changed items:

js
Copy to Clipboard
function toUpper(string) {
  return string.toUpperCase();
}

const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];

const upperCats = cats.map(toUpper);

console.log(upperCats);
// [ "LEOPARD", "SERVAL", "JAGUAR", "TIGER", "CARACAL", "LION" ]
Here we pass a function into cats.map(), and map() calls the function once for each item in the array, passing in the item. It then adds the return value from each function call to a new array, and finally returns the new array. In this case the function we provide converts the item to uppercase, so the resulting array contains all our cats in uppercase:

js
Copy to Clipboard
[ "LEOPARD", "SERVAL", "JAGUAR", "TIGER", "CARACAL", "LION" ]

You can use filter() to test each item in a collection, and create a new collection containing only items that match:

js
Copy to Clipboard
function lCat(cat) {
  return cat.startsWith("L");
}

const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];

const filtered = cats.filter(lCat);

console.log(filtered);
// [ "Leopard", "Lion" ]
This looks a lot like map(), except the function we pass in returns a boolean: if it returns true, then the item is included in the new array. Our function tests that the item starts with the letter "L", so the result is an array containing only cats whose names start with "L":

js
Copy to Clipboard
[ "Leopard", "Lion" ]

Note that map() and filter() are both often used with function expressions, which we will learn about in the Functions module. Using function expressions we could rewrite the example above to be much more compact:

js
Copy to Clipboard
const cats = ["Leopard", "Serval", "Jaguar", "Tiger", "Caracal", "Lion"];

const filtered = cats.filter((cat) => cat.startsWith("L"));
console.log(filtered);
// [ "Leopard", "Lion" ]

-------------------------------------------------
  
The standard for loop
In the "drawing circles" example above, you don't have a collection of items to loop through: you really just want to run the same code 100 times. In a case like that, you should use the for loop. This has the following syntax:

js
Copy to Clipboard
for (initializer; condition; final-expression) {
  // code to run
}
Here we have:

The keyword for, followed by some parentheses.
Inside the parentheses we have three items, separated by semicolons:
An initializer — this is usually a variable set to a number, which is incremented to count the number of times the loop has run. It is also sometimes referred to as a counter variable.
A condition — this defines when the loop should stop looping. This is generally an expression featuring a comparison operator, a test to see if the exit condition has been met.
A final-expression — this is always evaluated (or run) each time the loop has gone through a full iteration. It usually serves to increment (or in some cases decrement) the counter variable, to bring it closer to the point where the condition is no longer true.
Some curly braces that contain a block of code — this code will be run each time the loop iterates.

-------------------------------------------------

Exiting loops with break
If you want to exit a loop before all the iterations have been completed, you can use the break statement. We already met this in the previous article when we looked at switch statements — when a case is met in a switch statement that matches the input expression, the break statement immediately exits the switch statement and moves on to the code after it.

It's the same with loops — a break statement will immediately exit the loop and make the browser move on to any code that follows it.
  
Skipping iterations with continue
The continue statement works similarly to break, but instead of breaking out of the loop entirely, it skips to the next iteration of the loop. Let's look at another example that takes a number as an input, and returns only the numbers that are squares of integers (whole numbers).
  
-------------------------------------------------
  

while and do...while
for is not the only type of loop available in JavaScript. There are actually many others and, while you don't need to understand all of these now, it is worth having a look at the structure of a couple of others so that you can recognize the same features at work in a slightly different way.

First, let's have a look at the while loop. This loop's syntax looks like so:

js
Copy to Clipboard
initializer
while (condition) {
  // code to run

  final-expression
}
This works in a very similar way to the for loop, except that the initializer variable is set before the loop, and the final-expression is included inside the loop after the code to run, rather than these two items being included inside the parentheses. The condition is included inside the parentheses, which are preceded by the while keyword rather than for.

The same three items are still present, and they are still defined in the same order as they are in the for loop. This is because you must have an initializer defined before you can check whether or not the condition is true. The final-expression is then run after the code inside the loop has run (an iteration has been completed), which will only happen if the condition is still true.


The do...while loop is very similar, but provides a variation on the while structure:

js
Copy to Clipboard
initializer
do {
  // code to run

  final-expression
} while (condition)
In this case, the initializer again comes first, before the loop starts. The keyword directly precedes the curly braces containing the code to run and the final expression.

The main difference between a do...while loop and a while loop is that the code inside a do...while loop is always executed at least once. That's because the condition comes after the code inside the loop. So we always run that code, then check to see if we need to run it again. In while and for loops, the check comes first, so the code might never be executed.
    
    
Which loop type should you use?
If you're iterating through an array or some other object that supports it, and don't need access to the index position of each item, then for...of is the best choice. It's easier to read and there's less to go wrong.

For other uses, for, while, and do...while loops are largely interchangeable. They can all be used to solve the same problems, and which one you use will largely depend on your personal preference — which one you find easiest to remember or most intuitive. We would recommend for, at least to begin with, as it is probably the easiest for remembering everything — the initializer, condition, and final-expression all have to go neatly into the parentheses, so it is easy to see where they are and check that you aren't missing them.
The map method
map is one such function. It expects a callback as an argument, which is a fancy way to say “I want you to pass another function as an argument to my function”.

Let’s say we had a function addOne, which takes in num as an argument and outputs that num increased by 1. And let’s say we had an array of numbers, [1, 2, 3, 4, 5] and we’d like to increment all of these numbers by 1 using our addOne function. Instead of making a for loop and iterating over the above array, we could use our map array method instead, which automatically iterates over an array for us. We don’t need to do any extra work aside from simply passing the function we want to use in:

function addOne(num) {
  return num + 1;
}
const arr = [1, 2, 3, 4, 5];
const mappedArr = arr.map(addOne);
console.log(mappedArr); // Outputs [2, 3, 4, 5, 6]
map returns a new array and does not change the original array.

// The original array has not been changed!
console.log(arr); // Outputs [1, 2, 3, 4, 5]
This is a much more elegant approach, what do you think? For simplicity, we could also define an inline function right inside of map like so:

const arr = [1, 2, 3, 4, 5];
const mappedArr = arr.map((num) => num + 1);
console.log(mappedArr); // Outputs [2, 3, 4, 5, 6]

-----------------------------------------------------

The filter method
filter is somewhat similar to map. It still iterates through the array and applies the callback function on every item. However, instead of transforming the values in the array, it returns the original values of the array, but only IF the callback function returns true. Let’s say we had a function, isOdd that returns either true if a number is odd or false if it isn’t.

The filter method expects the callback to return either true or false. If it returns true, the value is included in the output. Otherwise, it isn’t. Consider the array from our previous example, [1, 2, 3, 4, 5]. If we wanted to remove all even numbers from this array, we could use .filter() like this:

function isOdd(num) {
  return num % 2 !== 0;
}
const arr = [1, 2, 3, 4, 5];
const oddNums = arr.filter(isOdd);
console.log(oddNums); // Outputs [1, 3, 5];
console.log(arr); // Outputs [1, 2, 3, 4, 5], original array is not affected
filter will iterate through arr and pass every value into the isOdd callback function, one at a time.
isOdd will return true when the value is odd, which means this value is included in the output.
If it’s an even number, isOdd will return false and not include it in the final output.

-----------------------------------------------------
  
The reduce method
Finally, let’s say that we wanted to multiply all of the numbers in our arr together like this: 1 * 2 * 3 * 4 * 5. First, we’d have to declare a variable total and initialize it to 1. Then, we’d iterate through the array with a for loop and multiply the total by the current number.

But we don’t actually need to do all of that, we have our reduce method that will do the job for us. Just like .map() and .filter() it expects a callback function. However, there are two key differences with this array method:

The callback function takes two arguments instead of one. The first argument is the accumulator, which is the current value of the result at that point in the loop. The first time through, this value will either be set to the initialValue (described in the next bullet point), or the first element in the array if no initialValue is provided. The second argument for the callback is the current value, which is the item currently being iterated on.
It also takes in an initialValue as a second argument (after the callback), which helps when we don’t want our initial value to be the first element in the array. For instance, if we wanted to sum all numbers in an array, we could call reduce without an initialValue, but if we wanted to sum all numbers in an array and add 10, we could use 10 as our initialValue.
const arr = [1, 2, 3, 4, 5];
const productOfAllNums = arr.reduce((total, currentItem) => {
  return total * currentItem;
}, 1);
console.log(productOfAllNums); // Outputs 120;
console.log(arr); // Outputs [1, 2, 3, 4, 5]
In the above function, we:

Pass in a callback function, which is (total, currentItem) => total * currentItem.
Initialize total to 1 in the second argument.
So what .reduce() will do, is it will once again go through every element in arr and apply the callback function to it. It then changes total, without actually changing the array itself. After it’s done, it returns total.
app.post('/send-otp', async (req, res) => {
  const { phoneNumber } = req.body;

  const apiKey = '3mW3hfluK8dpayQ53NXKdBhophrKP9sD8GKPi8qKqsMTZAAEsyq8HGMZCeSv';
  const otp = Math.floor(100000 + Math.random() * 900000); // Generate a 6-digit OTP
  const message = `${otp}`;

  const data = {
      sender_id: 'TKSOLV',
      message: '110131',
      variables_values: message,
      route: 'dlt',
      numbers: phoneNumber,
  };

  const options = {
      method: 'POST',
      headers: {
          'authorization': apiKey,
          'Content-Type': 'application/json'
      },
      data: JSON.stringify(data),
      url: 'https://www.fast2sms.com/dev/bulkV2',
  };

  try {
      const response = await axios(options);
      res.status(200).send({ success: true, message: 'OTP sent successfully', otp: otp });
  } catch (error) {
      res.status(500).send({ success: false, message: 'Failed to send OTP', error: error.message });
  }
});

// Try this simple way

const today = new Date();
let date = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate();
console.log(date);
//You can copy a JSON list of all the extensions and their URLs by going to chrome://extensions and entering this in your console
document.querySelector('extensions-manager').extensions_.map(({id, name, state, webStoreUrl}) => ({id, name, state, webStoreUrl}))
// App.tsx
import React, { useState } from 'react';
import './App.css';

function App() {
  const [top, setTop] = useState(0);
  const [left, setLeft] = useState(0);
  const [backgroundColor, setBackgroundColor] = useState('#ffffff')

  const keyDownHandler = (e: React.KeyboardEvent<HTMLDivElement>) => {
    const key = e.code;

    if (key === 'ArrowUp') {
      setTop((top) => top - 10);
    }

    if (key === 'ArrowDown') {
      setTop((top) => top + 10);
    }

    if (key === 'ArrowLeft') {
      setLeft((left) => left - 10);
    }

    if (key === 'ArrowRight') {
      setLeft((left) => left + 10);
    }

    if (key === 'Space') {
      let color = Math.floor(Math.random() * 0xFFFFFF).toString(16);
      for(let count = color.length; count < 6; count++) {
        color = '0' + color;                     
      }
      setBackgroundColor('#' + color);
    }
  }

  return (
    <div
      className="container"
      tabIndex={0}
      onKeyDown={keyDownHandler}
    >
      <div
        className="box"
        style={{ 
          top: top,
          left: left,
          backgroundColor: backgroundColor,
        }}></div>
    </div>
  );
}

export default App;
export const handleCacheListHolterBeats = async (studyId, profileId, timeRanges, ecgDataMap = []) => {
  try {
    if (!timeRanges || !timeRanges.length) return;

    const listHolterBeatsFilter = {
      studyId,
      profileId,
      timeRanges,
      ecgDataMap,
    };

    let listHolterBeats = [];

    for (const timeRange of timeRanges) {
      const { start, stop } = timeRange;
      if (!start || !stop) continue;

      const momentObject = {
        startMoment: start,
        stopMoment: stop,
      };

      // Await the result of checkAndProcessBeatData if it returns a promise
      const data = await checkAndProcessBeatData({
        ecgDataMap,
        momentObject,
      });

      listHolterBeats = listHolterBeats.concat(data);
    }

    // if (!listHolterBeats.length) {
    //   // Await the result of fetchListHolterBeats if it returns a promise
    //   listHolterBeats = await fetchListHolterBeats(listHolterBeatsFilter, KEY_CANCEL.API_HOLTER_AI);
    // }

    console.log('listHolterBeats: ', timeRanges, listHolterBeats);

    const client = await createClient();

    for (const item of listHolterBeats) {
      await client.writeQuery({
        query: HOLTER_BEAT_QUERY,
        data: {
          holterBeats: {
            isSuccess: true,
            message: null,
            beats: item.beats,
            hesBeatStatus: item.hesBeatStatus,
            beatChannels: item.beatChannels,
          },
        },
        variables: {
          filter: {
            studyId,
            profileId,
            start: item.start,
            stop: item.stop,
          },
        },
      });
    }

    console.log('Final Holter Beats Data:', listHolterBeats);
  } catch (error) {
    logError('Failed to cache list holter beats: ', error);
  }
};
function sum(...numbers) {
  // The rest operator is three dots followed by the variable name; by convention, it is typically called 'rest'
  // The rest operator must be the last parameter in the function definition
  return numbers.reduce((acc, val) => acc + val, 0);
}   // The reduce method is used to sum all the numbers in the array
/* eslint-disable no-bitwise */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-loop-func */
/* eslint-disable no-restricted-syntax */
import axios from 'axios';
import _ from 'lodash';
import { expose } from 'threads/worker';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';

dayjs.extend(isBetween); // Use the isBetween plugin for dayjs
const config = {};

const MAX_RETRY_ATTEMPTS = 2; // Số lần thử lại tối đa khi link download thất baị

const downloadFile = async (url) => {
  try {
    const response = await axios.get(url, { responseType: 'arraybuffer' });
    let { data } = response;
    if (data.byteLength % 5 !== 0) {
      data = data.slice(0, data.byteLength - (data.byteLength % 5));
    }
    return { data };
  } catch (error) {
    console.error('Failed to download file:', error);
    return { error: error.message }; // Return error message if the download fails
  }
};

const retryDownloadFile = async (url, attempts = MAX_RETRY_ATTEMPTS) => {
  let response = null;
  for (let i = 0; i < attempts; i += 1) {
    try {
      response = await axios.get(url, { responseType: 'arraybuffer' });
      if (response.data && (response.data.byteLength > 0 || response.data.length > 0)) {
        let { data } = response;
        if (data.byteLength % 5 !== 0) {
          data = data.slice(0, data.byteLength - (data.byteLength % 5));
        }
        return { data };
      }
    } catch (error) {
      console.error(`Retry attempt ${i + 1} failed for URL: ${url}`, error);
    }
  }
  return { error: `All retry attempts failed for URL: ${url}` }; // Return error message if all retry attempts fail
};

const processChunkedArray = (chunkedArray, start, stop) => {
  const beatData = {
    start,
    stop,
    beatPositions: [],
    beatEpochs: [],
    hesBeatStatus: [],
  };

  chunkedArray.forEach((item) => {
    const beatBytes = item.slice(2, 5).map(byte => (byte < 0 ? 256 + byte : byte));
    const beatPosition = (beatBytes[0] << 16) + (beatBytes[1] << 8) + beatBytes[2];
    const beatEpoch = dayjs(start).add(beatPosition / config.samplingFrequency, 'second').valueOf();
    beatData.beatPositions.push(beatPosition);
    beatData.beatEpochs.push(beatEpoch);
    beatData.hesBeatStatus.push(item[1]);
  });

  return beatData;
};

const processBeatData = (data, start, stop) => {
  const array = new Int8Array(data);
  const chunkedArray = _.chunk(array, 5);
  return processChunkedArray(chunkedArray, start, stop);
};

expose({
  createConfig: ({ samplingFrequency, querySignature }) => {
    config.samplingFrequency = samplingFrequency;
    config.querySignature = querySignature;
  },
  downloadSingleBeatFile: async (items) => {
    try {
      const pendingDownloads = [];
      const results = [];

      for (const item of items) {
        const { start, stop, aiPredict: { beatFinalPath } } = item;
        const trackedPromise = downloadFile(beatFinalPath)
          .then((result) => {
            if (result.data) {
              const processedData = processBeatData(result.data, start, stop);
              results.push(processedData);
            } else {
              console.error(`Failed to download file for item ID: ${item.id}`, result.error);
            }
          })
          .catch((error) => {
            console.error(`Failed to download file for item ID: ${item.id}`, error);
          });

        pendingDownloads.push(trackedPromise);
      }

      await Promise.all(pendingDownloads);

      const finalResult = {
        success: true, data: results,
      };

      return finalResult;
    } catch (error) {
      console.error('Failed to download and process single beat files:', error);
      return { success: false, message: error.message };
    }
  },

  downloadBeatListByDay: async (dateEpoch, data) => {
    try {
      const pendingDownloads = [];
      let completed = 0;
      const total = data.length;
      const results = [];
      let failedLinks = []; // Array to store failed link IDs

      const incrementCompleted = () => {
        completed += 1;
        return completed;
      };

      for (const [i, fileUrl] of data.entries()) {
        const { start, stop, id } = data[i];
        const keyStart = dayjs(start).startOf('hour').valueOf();
        const url = fileUrl?.aiPredict?.beatFinalPath;
        const trackedPromise = downloadFile(url)
          .then((result) => {
            if (result.data && (result.data.byteLength > 0 || result.data.length > 0)) {
              results.push({ key: keyStart, value: processBeatData(result.data, start, stop) });
              incrementCompleted();
            } else {
              failedLinks.push({ id, url, error: result.error || 'Empty or invalid response' }); // Store failed link with ID, URL, and error message
            }
          })
          .catch((error) => {
            console.error('Failed to download Beat list:', error);
            failedLinks.push({ id, url, error: error.message }); // Store failed link with ID, URL, and error message
            incrementCompleted();
          });

        pendingDownloads.push(trackedPromise);
      }

      await Promise.all(pendingDownloads);

      // Retry failed links
      for (const failedLink of failedLinks) {
        const { id, url } = failedLink;
        const result = await retryDownloadFile(url);
        if (result.data) {
          const keyStart = dayjs(data.find(item => item.id === id).start).startOf('hour').valueOf();
          results.push({ key: keyStart, value: processBeatData(result.data, data.find(item => item.id === id).start, data.find(item => item.id === id).stop) });
          failedLinks = failedLinks.filter(item => item.id !== id); // Remove successfully retried link
        } else {
          console.error('Failed to retry download of failed beat link:', id, url, result.error);
        }
      }

      const status = (completed / total) * 100;
      const metadata = {
        dateEpoch: +dateEpoch,
        status,
        total,
        completed,
        remaining: total - completed,
        failedLinks: failedLinks.map(link => ({ id: link.id, error: link.error })), // Include failed link IDs and errors in metadata
      };
      const finalResult = { result: results, metadata };
      postMessage({ type: 'beatData', data: finalResult });
      return 'done';
    } catch (error) {
      console.error('Failed to download beat list:', error);
      return [];
    }
  },

  downloadECGListByDay: async (dateEpoch, data) => {
    try {
      const pendingDownloads = [];
      const { querySignature } = config;
      let completed = 0;
      const total = data.length;
      const results = [];
      let failedLinks = []; // Array to store failed link IDs

      const incrementCompleted = () => {
        completed += 1;
        return completed;
      };

      for (const itemECG of data) {
        const url = `${itemECG.dataUrl}?${querySignature}`;
        const key = dayjs(itemECG.start.$d).startOf('hour').valueOf();

        const trackedPromise = downloadFile(url)
          .then((result) => {
            if (result.data && (result.data.byteLength > 0 || result.data.length > 0)) {
              results.push({ key, value: result.data });
              incrementCompleted();
            } else {
              failedLinks.push({ id: itemECG.id, url, error: result.error || 'Empty or invalid response' }); // Store failed link with ID, URL, and error message
            }
          })
          .catch((error) => {
            console.error('Failed to download ECG list:', error);
            failedLinks.push({ id: itemECG.id, url, error: error.message }); // Store failed link with ID, URL, and error message
            incrementCompleted();
          });

        pendingDownloads.push(trackedPromise);
      }

      await Promise.all(pendingDownloads);

      // Retry failed links
      for (const failedLink of failedLinks) {
        const { id, url } = failedLink;
        const result = await retryDownloadFile(url);
        if (result.data) {
          const key = dayjs(data.find(item => item.id === id).start.$d).startOf('hour').valueOf();
          results.push({ key, value: result.data });
          failedLinks = failedLinks.filter(item => item.id !== id); // Remove successfully retried link
        } else {
          console.error('Failed to retry download of failed ecg link:', id, url, result.error);
        }
      }

      const status = (completed / total) * 100;
      const metadata = {
        dateEpoch: +dateEpoch,
        status,
        total,
        completed,
        remaining: total - completed,
        failedLinks: failedLinks.map(link => ({ id: link.id, error: link.error })), // Include failed link IDs and errors in metadata
      };
      const finalResult = { result: results, metadata };
      postMessage({ type: 'ecgData', data: finalResult });
      return 'done';
    } catch (error) {
      console.error('Failed to download ECG list:', error);
      return [];
    }
  },
});
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
/* eslint-disable import/no-webpack-loader-syntax */
/* eslint-disable import/no-unresolved */
import classnames from 'classnames';
import dayjs from 'dayjs';
import _ from 'lodash';
import React, {
  startTransition,
  useEffect,
  useRef,
  useState,
} from 'react';
import {
  useHistory,
  useParams,
} from 'react-router-dom';
import {
  TabContent,
  TabPane,
  UncontrolledTooltip,
} from 'reactstrap';
import {
  useRecoilState,
  useRecoilValue,
  useSetRecoilState,
} from 'recoil';
import { t } from 'ttag';
import {
  clearInterval,
  setInterval,
} from 'worker-timers';
import {
  Thread,
  Worker,
  spawn,
} from 'threads';
import beatWorker from 'threads-plugin/dist/loader?name=beats!../../Worker/beats.worker';
import fetchHolterBeatEventsCount from '../../Apollo/Functions/fetchHolterBeatEventsCount';
import fetchHolterEcgDataMap from '../../Apollo/Functions/fetchHolterEcgDataMap';
import fetchHolterOtherEventsCount from '../../Apollo/Functions/fetchHolterOtherEventsCount';
import fetchHolterRhythmEventsCount from '../../Apollo/Functions/fetchHolterRhythmEventsCount';
import fetchHolterTasks from '../../Apollo/Functions/fetchHolterTasks';
import fetchListEcgBookmarks from '../../Apollo/Functions/fetchListEcgBookmarks';
import fetchNewReportData from '../../Apollo/Functions/fetchNewReportData';
import fetchQuerySignature from '../../Apollo/Functions/fetchQuerySignature';
import handleMarkReportAsArtifactReport from '../../Apollo/Functions/handleMarkReportAsArtifactReport';
import handleUpdateReportStatusV2 from '../../Apollo/Functions/handleUpdateReportStatusV2';
import IconButton from '../../ComponentsV2/Buttons/iconButton';
import ConfirmModal from '../../ComponentsV2/ConfirmModal';
import FacilityInfoDrawer from '../../ComponentsV2/Drawers/facilityInfoDrawer';
import PatientInfoDrawer from '../../ComponentsV2/Drawers/patientInfoDrawer';
import StudyInfoDrawer from '../../ComponentsV2/Drawers/studyInfoDrawer';
import {
  // resetEcgViewerConfigStore,
  ECG_VIEWER_CONFIG_STORE,
} from '../../ComponentsV2/FabricJS/Constants';
import {
  CALL_CENTER_PORTAL_URL,
  EMITTER_CONSTANTS,
} from '../../ConstantsV2';
import {
  // OTHER_EVENT_TYPE_OBJECT,
  // OTHER_EVENT_TYPES,
  IS_NOT_EDIT_REPORT_MESSAGE,
} from '../../ConstantsV2/aiConstants';
import LOADING_TYPES from '../../ConstantsV2/loadingTypes';
import AiPDFReportTab from '../../LayoutV2/Reports/AiEditReport/AiPdfReport';
import BeatHR from '../../LayoutV2/Reports/AiEditReport/BeatHR';
import { combinedEventsInHeatmapState } from '../../LayoutV2/Reports/AiEditReport/BeatHR/recoil';
import {
  actionSnapshotRecoilState,
  ecgBookmarksState,
  ecgDataMapState,
  facilityNoteState,
  findingsState,
  isLoadedIndexDBState,
  isOpenProcessingLimitState,
  isShowTabHeaderState,
  isStartedAiProcessingState,
  originalDailySummariesState,
  profileAfibAvgHrState,
  reportDataState,
  reportInfoState,
  isUndoDisabledState,
  highPassThumbnailState,
  channelsThumbnailState,
  gainThumbnailState,
  sizeEventThumbnailConfigState,
} from '../../LayoutV2/Reports/AiEditReport/Recoil';
import {
  defaultReportOptions,
  reportOptionsState,
} from '../../LayoutV2/Reports/AiEditReport/AiPdfReport/recoil';
import eventEmitter from '../../UtilsV2/eventEmitter';
import RhythmEvents from '../../LayoutV2/Reports/AiEditReport/RhythmEvents';
import {
  beatEventCountState,
  otherEventCountState,
  rhythmEventCountState,
} from '../../LayoutV2/Reports/AiEditReport/RhythmEvents/recoil';
import StripsManagement from '../../LayoutV2/Reports/AiEditReport/StripsManagement';
import {
  CONFIG_ECG,
  REPORT_TYPE,
  logError,
} from '../../LayoutV2/Reports/AiEditReport/handler';
import { fetchHolterOtherEventsTypesRequest } from '../../ReduxV2/Actions/holterOtherEventsTypes';
import loadingPage from '../../ReduxV2/Actions/loading';
import closeIcon from '../../StaticV2/Images/Components/close-icon.svg';
import icArtifactActive from '../../StaticV2/Images/Components/ic-artifact-active.svg';
import icArtifact from '../../StaticV2/Images/Components/ic-artifact.svg';
import {
  timezoneToOffset,
  zeroPad,
  simulateKeyEvent,
} from '../../UtilsV2';
import {
  generateAiStatusReport,
  generateListDate,
  isHolterProcessingAvailable,
} from '../../UtilsV2/aiUtils';
import auth from '../../UtilsV2/auth';
import {
  useActions,
  useEmitter,
  useGetRecoilValue,
  useMergeState,
} from '../../UtilsV2/customHooks';
import logHandler from '../../UtilsV2/logHandler';
import socketio from '../../UtilsV2/socketio';
import {
  toastrError,
  toastrSuccess,
} from '../../UtilsV2/toastNotification';
import CheckSavePrompt from './CheckSavePrompt';
import ReportHeaderWrapper from './ReportHeader/reportHeaderWrapper';
import ReviewedReportIcon from './assets/reviewed-report-icon.svg';
import UnreviewedReportIcon from './assets/unreviewed-report-icon.svg';
import UndoIcon from './assets/undo-icon.svg';
import DebugObserver from './debugObserver';
import { clearAllDataStateRecoil, getDataStateRecoil, upsertDataStateRecoil } from '../../Store/dbStateRecoil';
import { resetHistoryStateRecoil } from '../../Store/dbHistoryStateRecoil';
import InitRecoilFromIndexDB from '../../LayoutV2/Reports/AiEditReport/Recoil/InitRecoilFromIndexDB';
import handleUpdateHolterProfile from '../../Apollo/Functions/handleUpdateHolterProfile';
import UndoStateRecoil from '../../LayoutV2/Reports/AiEditReport/Recoil/undoStateRecoil';
import ProgressLoading from '../../LayoutV2/Reports/AiEditReport/SharedModule/progressLoading';
import { checkHasChangedSavedIndexDB } from './handler';

// IndexDB for Beat Data and ECG Data
import {
  addMultipleBeatData, addMetaBeatData, clearDBBeatData,
} from '../../Store/dbBeatData';
import {
  addMultipleECGData, addMetaEcgData, clearDBEcgData,
} from '../../Store/dbECGData';

import studySummariesJson from '../../../dummyData/studySummaries.json';
// eslint-disable-next-line import/no-webpack-loader-syntax

/*
  All hot keys:
  N,S,V,Q,Z,X,C,W,F,ESC,B,CTRL+A,CTRL+S,D,R,A,arrow up down left right,>,<,1,2,3,4,5,6
  N,S,V,Q: change beat type
  D: delete beat/event
  Z: previous page
  X: next page
  C: open change duration event
  W: open add new event
  F: open add bookmark
  R: mark/unmark event reviewed
  ESC: escape from mode
  B: toggle Add Remove Beat mode (ECGViewer)
  A: mark event as artifact
  CTRL+A: select all strip
  CTRL+S: Save data
  arrow up down left right: navigate through strip in page
  1,2,3,4,5,6: set bulk action beat
*/
const AiReportPage = () => {
  const { studyFid } = useParams();

  const history = useHistory();

  const isMacOS = navigator.userAgent.indexOf('Mac') !== -1;
  const [loadingPageAction, fetchHolterOtherEventsTypesAction] = useActions([loadingPage, fetchHolterOtherEventsTypesRequest]);

  const defaultActiveTab = useRef('4');

  const [state, setState] = useMergeState({
    isLoadedData: false,
    isGeneratingNormalReportInCallCenter: false,
    isOpenStudyInfoDrawer: false,
    isOpenPatientInfoDrawer: false,
    isOpenFacilityInfoDrawer: false,
    isOpenNoActivityModal: false,
    isOpenConfirmArtifact: false,
    isOpenConfirmNavigateTab: false,
    isOpenUseDbIndexModal: false,
    isLoadingSaveAiReport: false,
    conflictUsers: [],
    activeTab: defaultActiveTab.current,
  });
  const [isNotEdit, setIsNotEdit] = useState(true);
  const [isLoadIndexDB, setIsLoadIndexDB] = useState(false);
  const [prevEcgDataMap, setPrevEcgDataMap] = useState(null);
  const [reportData, setReportData] = useRecoilState(reportDataState);
  const [reportInfo, setReportInfo] = useRecoilState(reportInfoState);
  const getReportInfo = useGetRecoilValue(reportInfoState);
  const [ecgDataMap, setEcgDataMap] = useRecoilState(ecgDataMapState);
  const isStartedAiProcessing = useRecoilValue(isStartedAiProcessingState);
  const [isOpenProcessingLimit, setIsOpenProcessingLimit] = useRecoilState(isOpenProcessingLimitState);
  const setFindings = useSetRecoilState(findingsState);
  const setFacilityNote = useSetRecoilState(facilityNoteState);
  const setOriginalDailySummaries = useSetRecoilState(originalDailySummariesState);
  const setProfileAfibAvgHr = useSetRecoilState(profileAfibAvgHrState);
  const isShowTabHeader = useRecoilValue(isShowTabHeaderState);
  const setCombinedEventsInHeatmap = useSetRecoilState(combinedEventsInHeatmapState);
  const setEcgBookmarks = useSetRecoilState(ecgBookmarksState);
  const setActionSnapshotRecoil = useSetRecoilState(actionSnapshotRecoilState);
  const setHolterBeatEventsCount = useSetRecoilState(beatEventCountState);
  const setHolterRhythmEventsCount = useSetRecoilState(rhythmEventCountState);
  const setHolterOtherEventsCount = useSetRecoilState(otherEventCountState);
  const setHighPassThumbnail = useSetRecoilState(highPassThumbnailState);
  const setSizeEventThumbnailConfig = useSetRecoilState(sizeEventThumbnailConfigState);
  const setGainThumbnail = useSetRecoilState(gainThumbnailState);
  const setChannelsThumbnail = useSetRecoilState(channelsThumbnailState);
  const setReportOptions = useSetRecoilState(reportOptionsState);

  const isLoadedIndexDB = useRecoilValue(isLoadedIndexDBState);
  const [isUndoDisabled, setIsUndoDisabled] = useRecoilState(isUndoDisabledState);
  useEffect(() => {
    // Set the default value to true
    setTimeout(() => {
      setIsUndoDisabled(true);
    }, 100);
    const updateIndex = (currentIndex) => {
      setIsUndoDisabled(currentIndex < 1);
    };
    eventEmitter.addListener(EMITTER_CONSTANTS.UNDO_REDO_CURRENT_INDEX, updateIndex);

    // Clean up the event listener on component unmount
    return () => {
      // eventEmitter.removeListener(EMITTER_CONSTANTS.UNDO_REDO_CURRENT_INDEX, updateIndex);
    };
  }, []);

  const {
    studyId,
    profileId,
    facilityId,
    reportId,
    isAbortedStudy,
    startReport,
    timezoneOffset,
  } = reportInfo;

  const roomReport = useRef();
  const updatingTabCount = useRef(0);
  const tabIndexConfirmNavigate = useRef();
  const tab1Ref = useRef();
  const tab2Ref = useRef();
  const tab3Ref = useRef();
  const tab4Ref = useRef();
  const findingRef = useRef();
  const ecgDisclosureRef = useRef();
  const sessionInfoRef = useRef();

  const fetchEventsCount = async () => {
    try {
      const eventsCountFilter = {
        studyId,
        profileId,
      };
      const promises = [
        fetchHolterBeatEventsCount(eventsCountFilter, false, null, false),
        fetchHolterRhythmEventsCount(eventsCountFilter, false, null, false),
        fetchHolterOtherEventsCount(eventsCountFilter, false, null, false),
      ];
      const [
        holterBeatEventsCount,
        holterRhythmEventsCount,
        holterOtherEventsCount,
      ] = await Promise.all(promises);
      setHolterBeatEventsCount(holterBeatEventsCount);
      setHolterRhythmEventsCount(holterRhythmEventsCount);
      setHolterOtherEventsCount(holterOtherEventsCount);
    } catch (error) {
      logError('Failed to fetch holter rhythm events count: ', error);
    }
  };

  const getDatabaseStateRecoil = async () => {
    const activeTab = await getDataStateRecoil('activeTab');
    if (activeTab?.value) {
      setState({ activeTab: activeTab?.value });
      defaultActiveTab.current = activeTab?.value;
    }
  };

  const restDefaultState = () => {
    setHighPassThumbnail(1);
    setGainThumbnail(7.5);
    setChannelsThumbnail([4]);
    setSizeEventThumbnailConfig({ name: 'Small', width: 415, height: 87.1 });
    setReportOptions(defaultReportOptions);
    upsertDataStateRecoil('hourlyBeatDataArray', []);
  };

  // TODO : handle clear all beat and ecg data
  const clearAllBeatData = async () => {
    await clearDBEcgData();
    await clearDBBeatData();
  };

  const clearDataAndResaveSessionInfo = async () => {
    await clearAllDataStateRecoil();
    clearAllBeatData();
    setFindings(findingRef.current);
    if (!isNotEdit && sessionInfoRef.current) {
      upsertDataStateRecoil('sessionInfo', sessionInfoRef.current);
      upsertDataStateRecoil('activeTab', state.activeTab);
    }
  };

  const handleClearIndexDB = async (info) => {
    try {
      const dataSave = await getDataStateRecoil('sessionInfo');
      if (dataSave?.value) {
        const { studyId, profileId, sessionTime } = info;
        if (dataSave?.value?.studyId !== studyId
          || dataSave?.value?.profileId !== profileId
          || dayjs(dataSave?.value?.sessionTime).valueOf() !== dayjs(sessionTime).valueOf()) {
          // clean all data
          clearDataAndResaveSessionInfo();
          setIsLoadIndexDB(true);
        } else {
          // show popup when stay in tab 1 or tab 2
          const activeTab = await getDataStateRecoil('activeTab');
          // check has change data
          const isChanged = await checkHasChangedSavedIndexDB({
            activeTab: activeTab?.value,
            currentFinding: findingRef.current,
            currentEcgDisclosure: ecgDisclosureRef.current,
          });
          if (isChanged) {
            setState({ isOpenUseDbIndexModal: true });
          } else {
            clearDataAndResaveSessionInfo();
            setIsLoadIndexDB(true);
          }
        }
      } else {
        setIsLoadIndexDB(true);
      }
    } catch (error) {
      console.error(error);
    }
  };

  const onDiscardChangeSavedData = () => {
    setState({ isOpenUseDbIndexModal: false });
    clearDataAndResaveSessionInfo();
    setIsLoadIndexDB(true);
  };

  const onKeepEditingWithSavedData = () => {
    setState({ isOpenUseDbIndexModal: false });
    setIsLoadIndexDB(true);
  };

  const fetchReportData = async (studyFid) => {
    try {
      const input = {
        studyFid: studyFid ? parseInt(studyFid, 10) : undefined,
        type: REPORT_TYPE,
      };
      const aiReportData = await fetchNewReportData({ input });
      const { reportData, isSuccess } = aiReportData;
      if (isSuccess && !_.isEmpty(reportData)) {
        if (reportData.study?.isArchived || generateAiStatusReport(reportData.study) !== 'Ready') {
          history.replace('/404');
          return;
        }
        const studyId = reportData.study?.id;
        const profileId = reportData.study?.holterProfile?.id;
        const [downloadEcgDataMap, holterTasks, studyReportsData] = await Promise.all([
          fetchHolterEcgDataMap({ studyId, profileId }),
          fetchHolterTasks(),
          fetchNewReportData({ id: studyId, isStudyReport: true }),
        ]);
        if (downloadEcgDataMap) {
          CONFIG_ECG.samplingFrequency = downloadEcgDataMap.samplingFrequency;
          CONFIG_ECG.gain = downloadEcgDataMap.gain;
          ECG_VIEWER_CONFIG_STORE.visibleChannels = _.map(downloadEcgDataMap.channels, item => Number(item.slice(-1)) - 1);
          ECG_VIEWER_CONFIG_STORE.originalChannels = downloadEcgDataMap.channels;
        }
        const studyReport = _.filter(studyReportsData, x => x.type === 'Study')[0];
        const aiReport = _.find(studyReportsData, x => x.type === 'AI EOU');
        socketio.emitRoom(EMITTER_CONSTANTS.HOLTER_GENERAL_ROOM);
        socketio.emitRoom(EMITTER_CONSTANTS.HOLTER_PROFILE_ROOM.replace('{profileId}', profileId));
        socketio.emitRoom(reportData.study?.device?.deviceId);
        // *: Join room report
        if (reportData?.report?.type === 'Study' || reportData.report?.type === 'AI EOU' || reportData?.report?.reportId) {
          const room = `${zeroPad(studyFid)}_ai_eou`;
          roomReport.current = room;
          socketio.sendJoinReportRoom(roomReport.current);
        }
        // isHolterReport: [STUDY_TYPE.HOLTER, STUDY_TYPE.EXTENDED_HOLTER, STUDY_TYPE.MCT_PEEK].includes(reportData.study?.studyType)
        const startReport = reportData?.report?.start;
        const stopReport = reportData?.report?.stop;
        const timezone = reportData.study?.timezone;
        const timezoneOffset = timezoneToOffset(timezone);
        const listDate = generateListDate(startReport, stopReport, timezoneOffset);
        const combinedEventsInHeatmap = {};
        _.forEach(listDate, (date) => {
          combinedEventsInHeatmap[date] = [];
        });
        setCombinedEventsInHeatmap(combinedEventsInHeatmap);
        const info = {
          studyId: reportData.study?.id,
          profileId: reportData.study?.holterProfile?.id,
          sessionTime: reportData.study?.holterProfile?.sessionTime,
          facilityId: reportData.study?.facility?.id,
          reportId: reportData.report?.id,
          reportFid: reportData.report?.reportId,
          isAbortedStudy: reportData.study?.status === 'Aborted',
          studyFid: reportData.study?.friendlyId,
          patientName: `${reportData.study?.info?.patient?.firstName} ${reportData.study?.info?.patient?.lastName}`,
          facilityName: reportData.study?.facility?.name,
          reportType: reportData.report?.type,
          timezone,
          timezoneOffset,
          startReport,
          stopReport,
        };
        setReportInfo(info);

        // *: Sort data by start time
        downloadEcgDataMap.data.sort((a, b) => a.start - b.start);
        for (let i = 0; i < downloadEcgDataMap.data.length; i += 1) {
          const item = downloadEcgDataMap.data[i];
          item.start = dayjs(item.start);
          item.stop = dayjs(item.stop);
        }

        findingRef.current = reportData?.report?.technicianComments || reportData?.study?.holterProfile?.technicianComments;
        ecgDisclosureRef.current = {
          ecgDisclosure: reportData.study?.holterProfile?.ecgDisclosure,
          timezoneOffset,
          ecgDataMap: downloadEcgDataMap,
        };
        // check clean IndexDB
        handleClearIndexDB(info);

        logHandler.setStudyId(studyId);
        setProfileAfibAvgHr(reportData.study?.holterProfile?.afibAvgHr);
        setOriginalDailySummaries(reportData.study?.holterProfile?.mctDailySummaries);
        setEcgDataMap(downloadEcgDataMap);
        // setFindings(reportData?.report?.technicianComments || reportData?.study?.holterProfile?.technicianComments);
        setFacilityNote(reportData?.study?.facility?.facilityNote);
        setReportData(reportData);
        setIsOpenProcessingLimit(!isHolterProcessingAvailable(holterTasks, reportData?.study?.friendlyId));
        setState({
          isLoadedData: true,
          isGeneratingNormalReportInCallCenter: studyReport?.status === 'Rendering',
          studyReport,
          aiReport,
        });
      } else {
        history.replace('/404');
      }
    } catch (error) {
      logError('Failed to load report data ', error);
      history.push('/404');
    }
    loadingPageAction();
  };

  const toggleDisplayStudyInfoDrawer = () => {
    setState({ isOpenStudyInfoDrawer: !state.isOpenStudyInfoDrawer });
  };

  const toggleDisplayPatientInfoDrawer = () => {
    setState({ isOpenPatientInfoDrawer: !state.isOpenPatientInfoDrawer });
  };

  const toggleDisplayFacilityInfoDrawer = () => {
    setState({ isOpenFacilityInfoDrawer: !state.isOpenFacilityInfoDrawer });
  };

  const onClickReload = () => {
    // *Cannot use window.location.reload() because it not work in Firefox
    //  window.location.reload();
    window.location.href = window.location.href;
  };

  const handleOnClickArtifactReport = async (flag) => {
    try {
      await handleMarkReportAsArtifactReport(reportId, flag);
    } catch (err) {
      toastrError(err, t`Error`);
    }
    setState({ isOpenConfirmArtifact: false });
  };

  const navigateTab = (tab) => {
    if (tab !== state.activeTab) {
      if (isNotEdit) {
        setState({ activeTab: tab || defaultActiveTab.current });
        return;
      }

      // check data changed
      const tabRef = {
        1: tab1Ref.current,
        2: tab2Ref.current,
        3: tab3Ref.current,
        4: tab4Ref.current,
      }[state.activeTab];
      const isChangedData = tabRef ? tabRef.isChangedData() : false;
      console.log('[aiReport]-navigateTab', isChangedData, tab);
      if (isChangedData) {
        tabIndexConfirmNavigate.current = tab;
        setState({ isOpenConfirmNavigateTab: true });
      } else {
        //* Do not reset config
        // resetEcgViewerConfigStore();
        tabIndexConfirmNavigate.current = undefined;
        setState({ activeTab: tab || defaultActiveTab.current, isOpenConfirmNavigateTab: false });
        setActionSnapshotRecoil(1);
        // reset undo/redo related components
        restDefaultState();
        setTimeout(() => {
          resetHistoryStateRecoil();
        }, 500);
      }
    }
  };

  const onClickCancelNavigateTab = () => {
    tabIndexConfirmNavigate.current = undefined;
    setState({ isOpenConfirmNavigateTab: false });
  };

  const onClickNavigateWithoutSave = () => {
    const tabRef = {
      1: tab1Ref.current,
      2: tab2Ref.current,
      3: tab3Ref.current,
      4: tab4Ref.current,
    }[state.activeTab];
    if (tabRef) {
      tabRef.resetData();
    }
    const tabNavigate = tabIndexConfirmNavigate.current;
    // reset undo/redo related components
    restDefaultState();
    setTimeout(() => {
      resetHistoryStateRecoil();
    }, 500);
    setTimeout(() => {
      navigateTab(tabNavigate);
    }, 20);
  };

  const onClickConfirmNavigateTab = () => {
    const tabRef = {
      1: tab1Ref.current,
      2: tab2Ref.current,
      3: tab3Ref.current,
      4: tab4Ref.current,
    }[state.activeTab];
    if (tabRef) {
      tabRef.onClickSaveReport();
    }
    // reset undo/redo related components
    restDefaultState();
    setTimeout(() => {
      resetHistoryStateRecoil();
    }, 500);
  };

  const saveReportOnClick = () => {
    const tabRef = {
      1: tab1Ref.current,
      2: tab2Ref.current,
      3: tab3Ref.current,
      4: tab4Ref.current,
    }[state.activeTab];
    if (tabRef) {
      tabRef.onClickSaveReport();
    }
    // reset undo/redo related components
    restDefaultState();
    setTimeout(() => {
      resetHistoryStateRecoil();
    }, 500);
  };

  const handleNoActivity = () => {
    if (window.isStopAiProcess) {
      return;
    }

    socketio.sendLeaveAllRooms();
    setState({ isOpenNoActivityModal: true });
  };

  const checkDataChange = () => {
    const result = (state.isOpenNoActivityModal ? false : (
      tab1Ref.current?.isChangedData?.()
      || tab2Ref.current?.isChangedData?.()
      || tab3Ref.current?.isChangedData?.()
      || tab4Ref.current?.isChangedData?.()
    ));
    console.log('[aiReport]-checkStudyReportDataChange', result, state.isOpenNoActivityModal);
    return result;
  };

  const renderMarkArtifact = () => (
    <div id="bt-artifact">
      <IconButton
        disabled={
          reportData?.study?.holterProfile?.isArtifactReport
          || isNotEdit
        }
        iconComponent={(
          <img
            src={
              (reportData?.study?.holterProfile?.isArtifactReport || reportData?.report?.isArtifactReport)
                ? icArtifactActive
                : icArtifact
            }
            alt={t`Artifact icon`}
          />
        )}
        onClick={() => {
          if (!reportData?.report?.isArtifactReport) {
            setState({ isOpenConfirmArtifact: true });
          } else {
            handleOnClickArtifactReport(!reportData?.report?.isArtifactReport);
          }
        }}
      />

      <UncontrolledTooltip
        target="bt-artifact"
        className="custom-tooltip"
        placement="bottom"
        delay={{ show: 0, hide: 0 }}
        hideArrow
      >
        {(() => {
          if (reportData?.study?.holterProfile?.isArtifactReport) return t`Artifact report due to no beats and events detected`;
          if (isNotEdit) return IS_NOT_EDIT_REPORT_MESSAGE;
          return reportData?.report?.isArtifactReport ? t`Unmark report as artifact` : t`Mark report as artifact`;
        })()}
      </UncontrolledTooltip>
    </div>
  );

  const handleMarkReviewedClick = async () => {
    if (_.isEmpty(state.studyReport) && _.isEmpty(state.aiReport)) {
      return;
    }

    const isReviewedAiReport = state.aiReport.status === 'Reviewed';
    try {
      const status = state.aiReport.status === 'Reviewed'
        ? 'Ready'
        : 'Reviewed';
      const aiReportFilter = {
        id: state.aiReport.id,
        status,
      };
      let reportFilter;
      const promises = [handleUpdateReportStatusV2(aiReportFilter)];
      if (state.studyReport) {
        reportFilter = {
          id: state.studyReport.id,
          status,
        };
        promises.push(handleUpdateReportStatusV2(reportFilter));
      }
      await Promise.all(promises);
      setState({
        studyReport: state.studyReport
          ? {
            ...state.studyReport,
            status: reportFilter.status,
          }
          : null,
        aiReport: {
          ...state.aiReport,
          status: aiReportFilter.status,
        },
      });
      toastrSuccess(
        isReviewedAiReport
          ? t`Mark as unreviewed`
          : t`Mark as reviewed`,
        t`Success`,
      );
    } catch (error) {
      console.error('Failed to mark reviewed', error);
      toastrError(
        isReviewedAiReport
          ? t`Failed to mark as unreviewed`
          : t`Failed to mark as reviewed`,
        t`Error`,
      );
    }
  };

  const renderMarkReviewed = () => {
    // const isReviewedReport = state.studyReport?.status === 'Reviewed';
    const isReviewedAiReport = state.aiReport?.status === 'Reviewed';
    return (
      <div id="bt-mark-reviewed">
        <IconButton
          iconComponent={(
            <img
              src={
                isReviewedAiReport
                  ? ReviewedReportIcon
                  : UnreviewedReportIcon
              }
              alt={t`Artifact icon`}
            />
          )}
          MarkAsReviewedIcon
          onClick={handleMarkReviewedClick}
        />

        <UncontrolledTooltip
          target="bt-mark-reviewed"
          className="custom-tooltip"
          placement="bottom"
          delay={{ show: 0, hide: 0 }}
          hideArrow
        >
          {
            isReviewedAiReport
              ? t`Mark as unreviewed`
              : t`Mark as reviewed`
          }
        </UncontrolledTooltip>
      </div>
    );
  };

  const renderUndoState = () => (
    <div id="bt-undo-btn">
      <IconButton
        className="more-icon mr-3"
        isOutline
        disabled={isUndoDisabled}
        iconComponent={(
          <img src={UndoIcon} alt={t`Undo icon`} />
          )}
        onClick={() => { simulateKeyEvent('Z', { ctrlKey: true }); }}
      />

      <UncontrolledTooltip
        target="bt-undo-btn"
        className="custom-tooltip"
        placement="bottom"
        delay={{ show: 0, hide: 0 }}
        hideArrow
      >
        {/* TODO: currently set disabled is true */}
        {isUndoDisabled ? t`There is nothing to undo` : (isMacOS ? t`Undo  ⌘ Z` : t`Undo  Ctrl Z`)}
      </UncontrolledTooltip>
    </div>
  );

  const handleFetchBookmarks = async () => {
    try {
      const ecgBookmarks = await fetchListEcgBookmarks({
        studyId,
        sortOrder: 'asc',
        skip: 0,
      }, 0);
      setEcgBookmarks(ecgBookmarks);
    } catch (error) {
      logError('Failed to fetch bookmarks ', error);
    }
  };

  useEffect(() => {
    if (isStartedAiProcessing) {
      fetchEventsCount();
      handleFetchBookmarks();
    }
  }, [isStartedAiProcessing]);

  useEffect(() => {
    let interval;
    //* Refresh query signature every 1 hour
    if (state.isLoadedData) {
      const fetchNewQuerySignature = async () => {
        try {
          const querySignature = await fetchQuerySignature({ studyId });
          startTransition(() => {
            setEcgDataMap(currentEcgDataMap => ({ ...currentEcgDataMap, querySignature }));
          });
        } catch (error) {
          console.log(error);
        }
      };
      interval = setInterval(() => {
        fetchNewQuerySignature();
      }, 3600000); // 1 hour
    }
    return () => {
      try {
        if (interval) {
          clearInterval(interval);
        }
      } catch (error) {
        console.log(error);
      }
    };
  }, [state.isLoadedData, studyId]);

  useEffect(() => {
    if (Number(studyFid)) {
      // get database local
      getDatabaseStateRecoil();
      fetchReportData(studyFid);

      // TODO: Fetch other holter events types
      // handleFetchHolterOtherEventsTypes();
      // fetchHolterOtherEventsTypesAction();
    } else {
      history.replace('/404');
    }
  }, [studyFid]);

  useEffect(() => {
    upsertDataStateRecoil('activeTab', state.activeTab);
  }, [state.activeTab]);

  useEffect(() => {
    if (!isNotEdit) {
      const info = getReportInfo();
      const sessionTime = dayjs();
      // update session time
      const sessionInfo = {
        sessionTime: sessionTime.toISOString(),
        studyId: info.studyId,
        profileId: info.profileId,
      };
      const updateHolterProfileInput = {
        id: info.profileId,
        sessionTime,
      };
      sessionInfoRef.current = sessionInfo;
      handleUpdateHolterProfile(updateHolterProfileInput);
      upsertDataStateRecoil('sessionInfo', sessionInfo);
    }
  }, [isNotEdit]);

  useEmitter(EMITTER_CONSTANTS.REPORT_UPDATED, (msg) => {
    const reportDataClone = _.cloneDeep(reportData);
    if (msg.studyFid === reportDataClone.study?.friendlyId) {
      if (msg.id === reportDataClone.report?.id) {
        _.assign(reportDataClone.report, msg);
        setReportData(reportDataClone);
      } else {
        // Report format cũ ở bên callcenter nếu đang generate sẽ chặn ko cho copy report AI
        setState({ isGeneratingNormalReportInCallCenter: msg.status === 'Rendering' });
      }
    }
  }, [reportData]);

  useEmitter(EMITTER_CONSTANTS.REPORT_ROOM_USER, async (msg) => {
    const updatedEvent = _.findLast(msg, x => x.room === roomReport.current);
    if (updatedEvent) {
      const { users } = updatedEvent;

      const accessToken = auth.getOriginalToken();
      // *: Check user can edit report
      let newIsNotEdit = true;
      const sortedConflictUsers = users?.sort((a, b) => (a.time - b.time));
      const firstUser = sortedConflictUsers?.length > 0 ? sortedConflictUsers[0] : {};
      if (!_.isEmpty(firstUser.userId) && !_.isEmpty(firstUser.token)) {
        if (firstUser.userId === auth.userId() && firstUser.token === (accessToken || '').slice(-10)) {
          newIsNotEdit = false;
        } else {
          newIsNotEdit = true;
        }
      } else {
        newIsNotEdit = true;
      }
      console.log('[aiReport]-SOCKETROOMUSER', users, accessToken, firstUser, newIsNotEdit);
      //* Reload 2nd user when 1st user exit
      if (isNotEdit && !newIsNotEdit && isStartedAiProcessing && state.conflictUsers.length > 1 && users.length === 1) {
        logHandler.addLog('reload', {
          users, conflictUsers: state.conflictUsers, isNotEdit, newIsNotEdit,
        });
        await logHandler.sendLog();
        onClickReload();
        return;
      }
      setIsNotEdit(newIsNotEdit);
      setState({ conflictUsers: sortedConflictUsers });
    }
  }, [isNotEdit, state.conflictUsers, isStartedAiProcessing]);

  useEmitter(EMITTER_CONSTANTS.HOLTER_PROFILE_UPDATED, (msg) => {
    const reportDataClone = _.cloneDeep(reportData);
    if (msg?.ecgDisclosure) {
      _.assign(reportDataClone.study?.holterProfile, { ecgDisclosure: msg.ecgDisclosure });
    }
    if (!_.isEmpty(msg?.reportConfiguration)) {
      _.assign(reportDataClone.study?.holterProfile, { reportConfiguration: msg.reportConfiguration });
    }
    setReportData(reportDataClone);
  }, [reportData]);

  useEmitter(EMITTER_CONSTANTS.AI_UPDATE_TAB, (msg) => {
    updatingTabCount.current += 1;
    console.log('[aiReport]-AI_UPDATE_TAB', updatingTabCount.current);
  }, []);

  useEmitter(EMITTER_CONSTANTS.AI_LOADING, (msg) => {
    // {
    //   isLoading: boolean,
    // }
    const { isLoading } = msg || {};
    console.log('[aiReport]-AI_LOADING', msg, updatingTabCount.current, tabIndexConfirmNavigate.current, state.isOpenConfirmNavigateTab);
    if (isLoading) {
      updatingTabCount.current = 0;
      setState({ isLoadingSaveAiReport: true });
    } else if (updatingTabCount.current > 0) {
      updatingTabCount.current -= 1;
      if (updatingTabCount.current === 0) {
        setState({ isLoadingSaveAiReport: false });
        const tabNavigate = tabIndexConfirmNavigate.current;
        if (state.isOpenConfirmNavigateTab && tabNavigate) {
          setTimeout(() => {
            navigateTab(tabNavigate);
          }, 20);
        }
      }
    } else {
      updatingTabCount.current = 0;
      setState({ isLoadingSaveAiReport: false });
      const tabNavigate = tabIndexConfirmNavigate.current;
      if (state.isOpenConfirmNavigateTab && tabNavigate) {
        setTimeout(() => {
          navigateTab(tabNavigate);
        }, 20);
      }
    }
  }, [state.isOpenConfirmNavigateTab]);

  useEmitter(EMITTER_CONSTANTS.COMMAND_PROGRESS_UPDATED, (msg) => {
    const {
      command, doneTasks, totalTasks, percentage,
    } = msg;
    if (command === 'update-report-data') {
      loadingPageAction(`${LOADING_TYPES.GENERATE_REPORT} ${percentage} %`);
    }
  }, []);

  useEmitter(EMITTER_CONSTANTS.EVENTSUPDATED_EVENT, (msg) => {
    if (isStartedAiProcessing) {
      fetchEventsCount();
    }
  }, [isStartedAiProcessing]);

  // Generic function to group data by epoch start of the day
  const groupDataByEpochStartOfDay = (data, dateKey) => data.reduce((acc, item) => {
    const dateEpoch = dayjs(item[dateKey]).startOf('day').valueOf();
    if (!acc[dateEpoch]) {
      acc[dateEpoch] = [];
    }
    acc[dateEpoch].push(item);
    return acc;
  }, {});


  const handleDownloadBeatList = async () => {
    let thread;

    try {
      // Initialize worker and spawn a thread
      const worker = new Worker(beatWorker);
      thread = await spawn(worker);

      // Configure the thread with necessary parameters
      await thread.createConfig({
        samplingFrequency: ecgDataMap.samplingFrequency,
        querySignature: ecgDataMap.querySignature,
      });

      // Define how the worker should handle incoming messages
      worker.onmessage = async (message) => {
        const { type, data } = message.data;
        if (type === 'beatData' || type === 'ecgData') {
          const { result, metadata } = data;
          console.log(`[beatWorker] ${type}`, result.length, metadata);

          if (metadata.failedLinks && metadata.failedLinks.length > 0) {
            metadata.failedLinks.forEach((link) => {
              console.error(`Failed to download file ${type} with ID: ${link.id}, Error: ${link.error}`);
            });
          }

          if (type === 'beatData') {
            await addMultipleBeatData(result);
            await addMetaBeatData(metadata.dateEpoch, metadata);
          } else if (type === 'ecgData') {
            await addMultipleECGData(result);
            await addMetaEcgData(metadata.dateEpoch, metadata);
          }
        }
      };

      // Group data by epoch start of the day
      const groupedDataBeat = groupDataByEpochStartOfDay(studySummariesJson, 'start');
      const groupedDataECG = groupDataByEpochStartOfDay(ecgDataMap.data, 'start');

      // Process beat data in worker
      for (const dateEpoch of Object.keys(groupedDataBeat)) {
        const data = groupedDataBeat[dateEpoch];
        await thread.downloadBeatListByDay(dateEpoch, data);
      }

      // Process ECG data in worker
      for (const dateEpoch of Object.keys(groupedDataECG)) {
        const data = groupedDataECG[dateEpoch];
        await thread.downloadECGListByDay(dateEpoch, data);
      }
    } catch (error) {
      console.error('Failed to download beat list:', error);
    } finally {
      // Terminate the worker thread
      if (thread) {
        // Thread.terminate(thread).catch(console.error);
      }
    }
  };

  useEffect(() => {
    if (
      ecgDataMap
      && Object.keys(ecgDataMap).length > 0
      && !_.isEqual(ecgDataMap.data.length, prevEcgDataMap?.data.length)
    ) {
      handleDownloadBeatList();
      setPrevEcgDataMap(ecgDataMap);
    }
  }, [ecgDataMap]);


  return (
    <>
      <CheckSavePrompt
        isNotEdit={isNotEdit}
        condition={checkDataChange}
      />
      <StudyInfoDrawer
        visible={state.isOpenStudyInfoDrawer}
        title={t`Study Information`}
        fetchStudyId={studyId}
        handleClickCloseButton={toggleDisplayStudyInfoDrawer}
      />
      <PatientInfoDrawer
        visible={state.isOpenPatientInfoDrawer}
        title={t`Patient Information`}
        studyId={studyId}
        handleClickCloseButton={toggleDisplayPatientInfoDrawer}
      />
      <FacilityInfoDrawer
        visible={state.isOpenFacilityInfoDrawer}
        title={t`Facility Information`}
        facilityId={facilityId}
        handleClickCloseButton={toggleDisplayFacilityInfoDrawer}
        isAbortedStudy={isAbortedStudy}
      />
      <ConfirmModal
        isOpen={isOpenProcessingLimit}
        onClickRightButton={() => { document.location.href = CALL_CENTER_PORTAL_URL; }}
        rightButtonName={t`Okay`}
        title={t`Approaching processing limit`}
        question={t`The current limit for viewing and processing reports by AI has been exceeded.
          Please try again later.`}
        isRedBtn={false}
      />
      <ConfirmModal
        isOpen={state.isOpenNoActivityModal}
        onClickRightButton={onClickReload}
        rightButtonName={t`Reload The Page`}
        title={t`Notification`}
        question={t`Since there has been no activity for the past 30 minutes, please reload the page to resume.`}
        isRedBtn={false}
      />
      <ConfirmModal
        isOpen={state.isOpenConfirmArtifact}
        onClickLeftButton={() => { setState({ isOpenConfirmArtifact: false }); }}
        onClickRightButton={() => handleOnClickArtifactReport(true)}
        leftButtonName={t`No`}
        rightButtonName={t`Yes`}
        title={t`Mark report as artifact`}
        question={t`Are you sure you want to mark this report as an "Artifact report"?`}
      />
      <ConfirmModal
        isOpen={state.isOpenUseDbIndexModal}
        className="unsaved-change-modal --use-db-index"
        onClickLeftButton={onDiscardChangeSavedData}
        onClickRightButton={onKeepEditingWithSavedData}
        leftButtonName={t`Discard changes`}
        rightButtonName={t`Keep editing`}
        title={t`Unsaved changes`}
        question={t`There are unsaved changes. If you would like to keep changes, press the “Keep editing” button below.`}
      />
      <ConfirmModal
        isOpen={state.isOpenConfirmNavigateTab}
        className="unsaved-change-modal"
        onClickLeftButton={onClickNavigateWithoutSave}
        onClickRightButton={onClickConfirmNavigateTab}
        leftButtonName={state.isLoadingSaveAiReport ? null : t`Don’t Save`}
        rightButtonName={t`Save Changes`}
        isFilledRightBtn
        isBoldLeftBtn
        title={(
          <div className="title-unsaved-change">
            <div>{t`Unsaved changes`}</div>
            <IconButton
              iconComponent={(
                <img
                  src={closeIcon}
                  alt={t`Close icon`}
                />
              )}
              onClick={onClickCancelNavigateTab}
            />
          </div>
        )}
        question={t`You're about to leave this tab with unsaved changes. What would you like to do before proceeding?`}
        isRedBtn={false}
        isSaving={state.isLoadingSaveAiReport}
      />
      <div
        className={classnames('custom-page')}
      >
        <ReportHeaderWrapper
          isLoadedData={state.isLoadedData}
          title={t`End of Use report`}
          conflictUsers={state.conflictUsers}
          toggleDisplayStudyInfoDrawer={toggleDisplayStudyInfoDrawer}
          toggleDisplayPatientInfoDrawer={toggleDisplayPatientInfoDrawer}
          toggleDisplayFacilityInfoDrawer={toggleDisplayFacilityInfoDrawer}
          markArtifactComponent={renderMarkArtifact()}
          markReviewedComponent={renderMarkReviewed()}
          undoStateComponent={renderUndoState()}
          isDisableSaveReport={isNotEdit || isAbortedStudy}
          onClickSaveReport={saveReportOnClick}
          isLoadingSaveAiReport={state.isLoadingSaveAiReport}
          isNotEdit={isNotEdit}
          profileId={profileId}
          studyId={studyId}
          isArtifactReport={reportData?.report?.isArtifactReport}
          activeTab={state.activeTab}
          navigateTab={navigateTab}
          isStartedAiProcessing={isStartedAiProcessing}
        />
        {
          state.isLoadedData && (
            <>
              {
                isLoadIndexDB && (
                  <InitRecoilFromIndexDB />
                )
              }
              {/* component undo and redo */}
              <UndoStateRecoil />

              <DebugObserver tab1Ref={tab1Ref} tab2Ref={tab2Ref} tab3Ref={tab3Ref} tab4Ref={tab4Ref} />
              <TabContent
                className={classnames('study-report-top-tab-content', isShowTabHeader ? '' : '--hide-tab')}
                activeTab={state.activeTab}
              >
                <TabPane tabId="1">
                  <BeatHR
                    tabRef={tab1Ref}
                    handleNoActivity={handleNoActivity}
                    activeTab={state.activeTab}
                  />
                </TabPane>

                <TabPane tabId="2">
                  <RhythmEvents
                    tabRef={tab2Ref}
                    handleNoActivity={handleNoActivity}
                    activeTab={state.activeTab}
                  />
                </TabPane>

                <TabPane tabId="3">
                  {isLoadedIndexDB ? (
                    <StripsManagement
                      ref={tab3Ref}
                      isActive={state.activeTab === '3'}
                      ecgDataMap={ecgDataMap}
                      reportData={reportData}
                      studyFid={parseInt(studyFid, 10)}
                      timezoneOffset={timezoneOffset}
                      startReport={startReport}
                      isNotEdit={isNotEdit}
                      reportType={REPORT_TYPE}
                    />
                  ) : <ProgressLoading />}
                </TabPane>

                <TabPane tabId="4">
                  {isLoadedIndexDB ? (
                    <AiPDFReportTab
                      ref={tab4Ref}
                      isActive={state.activeTab === '4'}
                      isAbortedStudy={isAbortedStudy}
                      isNotEdit={isNotEdit}
                      isGeneratingNormalReportInCallCenter={state.isGeneratingNormalReportInCallCenter}
                    />
                  ) : <ProgressLoading />}

                </TabPane>
              </TabContent>
            </>
          )
        }
      </div>
    </>
  );
};

AiReportPage.propTypes = {

};

export default AiReportPage;
You can use the async keyword to create an asynchronous function, which returns a promise.

Example Code
const example = async () => {
  console.log("this is an example");
};

The try block is designed to handle potential errors, and the code inside the catch block will be executed in case an error occurs.

Example Code
try {
  const name = "freeCodeCamp";
  name = "fCC";
} catch (err) {
  console.log(err); // TypeError: Assignment to constant variable.
}

The await keyword waits for a promise to resolve and returns the result.

Example Code
const example = async () => {
  const data = await fetch("https://example.com/api");
  console.log(data);
}
/* eslint-disable import/no-webpack-loader-syntax */
/* eslint-disable import/no-unresolved */
import classnames from 'classnames';
import dayjs from 'dayjs';
import _ from 'lodash';
import React, {
  startTransition,
  useEffect,
  useRef,
  useState,
} from 'react';
import {
  useHistory,
  useParams,
} from 'react-router-dom';
import {
  TabContent,
  TabPane,
  UncontrolledTooltip,
} from 'reactstrap';
import {
  useRecoilState,
  useRecoilValue,
  useSetRecoilState,
} from 'recoil';
import { c, t } from 'ttag';
import {
  clearInterval,
  setInterval,
} from 'worker-timers';
import {
  Thread,
  Worker,
  spawn,
} from 'threads';
import beatWorker from 'threads-plugin/dist/loader?name=beats!../../Worker/beats.worker';
import fetchHolterBeatEventsCount from '../../Apollo/Functions/fetchHolterBeatEventsCount';
import fetchHolterEcgDataMap from '../../Apollo/Functions/fetchHolterEcgDataMap';
import fetchHolterOtherEventsCount from '../../Apollo/Functions/fetchHolterOtherEventsCount';
import fetchHolterRhythmEventsCount from '../../Apollo/Functions/fetchHolterRhythmEventsCount';
import fetchHolterTasks from '../../Apollo/Functions/fetchHolterTasks';
import fetchListEcgBookmarks from '../../Apollo/Functions/fetchListEcgBookmarks';
import fetchNewReportData from '../../Apollo/Functions/fetchNewReportData';
import fetchQuerySignature from '../../Apollo/Functions/fetchQuerySignature';
import handleMarkReportAsArtifactReport from '../../Apollo/Functions/handleMarkReportAsArtifactReport';
import handleUpdateReportStatusV2 from '../../Apollo/Functions/handleUpdateReportStatusV2';
import IconButton from '../../ComponentsV2/Buttons/iconButton';
import ConfirmModal from '../../ComponentsV2/ConfirmModal';
import FacilityInfoDrawer from '../../ComponentsV2/Drawers/facilityInfoDrawer';
import PatientInfoDrawer from '../../ComponentsV2/Drawers/patientInfoDrawer';
import StudyInfoDrawer from '../../ComponentsV2/Drawers/studyInfoDrawer';
import {
  // resetEcgViewerConfigStore,
  ECG_VIEWER_CONFIG_STORE,
} from '../../ComponentsV2/FabricJS/Constants';
import {
  CALL_CENTER_PORTAL_URL,
  EMITTER_CONSTANTS,
} from '../../ConstantsV2';
import {
  // OTHER_EVENT_TYPE_OBJECT,
  // OTHER_EVENT_TYPES,
  IS_NOT_EDIT_REPORT_MESSAGE,
} from '../../ConstantsV2/aiConstants';
import LOADING_TYPES from '../../ConstantsV2/loadingTypes';
import AiPDFReportTab from '../../LayoutV2/Reports/AiEditReport/AiPdfReport';
import BeatHR from '../../LayoutV2/Reports/AiEditReport/BeatHR';
import { combinedEventsInHeatmapState } from '../../LayoutV2/Reports/AiEditReport/BeatHR/recoil';
import {
  actionSnapshotRecoilState,
  ecgBookmarksState,
  ecgDataMapState,
  facilityNoteState,
  findingsState,
  isLoadedIndexDBState,
  isOpenProcessingLimitState,
  isShowTabHeaderState,
  isStartedAiProcessingState,
  originalDailySummariesState,
  profileAfibAvgHrState,
  reportDataState,
  reportInfoState,
  isUndoDisabledState,
  highPassThumbnailState,
  channelsThumbnailState,
  gainThumbnailState,
  sizeEventThumbnailConfigState,
} from '../../LayoutV2/Reports/AiEditReport/Recoil';
import {
  defaultReportOptions,
  reportOptionsState,
} from '../../LayoutV2/Reports/AiEditReport/AiPdfReport/recoil';
import eventEmitter from '../../UtilsV2/eventEmitter';
import RhythmEvents from '../../LayoutV2/Reports/AiEditReport/RhythmEvents';
import {
  beatEventCountState,
  otherEventCountState,
  rhythmEventCountState,
} from '../../LayoutV2/Reports/AiEditReport/RhythmEvents/recoil';
import StripsManagement from '../../LayoutV2/Reports/AiEditReport/StripsManagement';
import {
  CONFIG_ECG,
  REPORT_TYPE,
  logError,
} from '../../LayoutV2/Reports/AiEditReport/handler';
import { fetchHolterOtherEventsTypesRequest } from '../../ReduxV2/Actions/holterOtherEventsTypes';
import loadingPage from '../../ReduxV2/Actions/loading';
import closeIcon from '../../StaticV2/Images/Components/close-icon.svg';
import icArtifactActive from '../../StaticV2/Images/Components/ic-artifact-active.svg';
import icArtifact from '../../StaticV2/Images/Components/ic-artifact.svg';
import {
  timezoneToOffset,
  zeroPad,
  simulateKeyEvent,
} from '../../UtilsV2';
import {
  generateAiStatusReport,
  generateListDate,
  isHolterProcessingAvailable,
} from '../../UtilsV2/aiUtils';
import auth from '../../UtilsV2/auth';
import {
  useActions,
  useEmitter,
  useGetRecoilValue,
  useMergeState,
} from '../../UtilsV2/customHooks';
import logHandler from '../../UtilsV2/logHandler';
import socketio from '../../UtilsV2/socketio';
import {
  toastrError,
  toastrSuccess,
} from '../../UtilsV2/toastNotification';
import CheckSavePrompt from './CheckSavePrompt';
import ReportHeaderWrapper from './ReportHeader/reportHeaderWrapper';
import ReviewedReportIcon from './assets/reviewed-report-icon.svg';
import UnreviewedReportIcon from './assets/unreviewed-report-icon.svg';
import UndoIcon from './assets/undo-icon.svg';
import DebugObserver from './debugObserver';
import { clearAllDataStateRecoil, getDataStateRecoil, upsertDataStateRecoil } from '../../Store/dbStateRecoil';
import { resetHistoryStateRecoil } from '../../Store/dbHistoryStateRecoil';
import InitRecoilFromIndexDB from '../../LayoutV2/Reports/AiEditReport/Recoil/InitRecoilFromIndexDB';
import handleUpdateHolterProfile from '../../Apollo/Functions/handleUpdateHolterProfile';
import UndoStateRecoil from '../../LayoutV2/Reports/AiEditReport/Recoil/undoStateRecoil';
import ProgressLoading from '../../LayoutV2/Reports/AiEditReport/SharedModule/progressLoading';
import { checkHasChangedSavedIndexDB } from './handler';

// IndexDB for Beat Data and ECG Data
import { openDBBeatData, addMultipleBeatData, addMetaBeatData } from '../../Store/dbBeatData';
import { openDBEcgData, addMultipleECGData, addMetaEcgData } from '../../Store/dbECGData';

import studySummariesJson from '../../../dummyData/studySummaries.json';
// eslint-disable-next-line import/no-webpack-loader-syntax

/*
  All hot keys:
  N,S,V,Q,Z,X,C,W,F,ESC,B,CTRL+A,CTRL+S,D,R,A,arrow up down left right,>,<,1,2,3,4,5,6
  N,S,V,Q: change beat type
  D: delete beat/event
  Z: previous page
  X: next page
  C: open change duration event
  W: open add new event
  F: open add bookmark
  R: mark/unmark event reviewed
  ESC: escape from mode
  B: toggle Add Remove Beat mode (ECGViewer)
  A: mark event as artifact
  CTRL+A: select all strip
  CTRL+S: Save data
  arrow up down left right: navigate through strip in page
  1,2,3,4,5,6: set bulk action beat
*/
const AiReportPage = () => {
  const { studyFid } = useParams();

  const history = useHistory();

  const isMacOS = navigator.userAgent.indexOf('Mac') !== -1;
  const [loadingPageAction, fetchHolterOtherEventsTypesAction] = useActions([loadingPage, fetchHolterOtherEventsTypesRequest]);

  const defaultActiveTab = useRef('4');

  const [state, setState] = useMergeState({
    isLoadedData: false,
    isGeneratingNormalReportInCallCenter: false,
    isOpenStudyInfoDrawer: false,
    isOpenPatientInfoDrawer: false,
    isOpenFacilityInfoDrawer: false,
    isOpenNoActivityModal: false,
    isOpenConfirmArtifact: false,
    isOpenConfirmNavigateTab: false,
    isOpenUseDbIndexModal: false,
    isLoadingSaveAiReport: false,
    conflictUsers: [],
    activeTab: defaultActiveTab.current,
  });
  const [isNotEdit, setIsNotEdit] = useState(true);
  const [isLoadIndexDB, setIsLoadIndexDB] = useState(false);
  const [prevEcgDataMap, setPrevEcgDataMap] = useState(null);
  const [reportData, setReportData] = useRecoilState(reportDataState);
  const [reportInfo, setReportInfo] = useRecoilState(reportInfoState);
  const getReportInfo = useGetRecoilValue(reportInfoState);
  const [ecgDataMap, setEcgDataMap] = useRecoilState(ecgDataMapState);
  const isStartedAiProcessing = useRecoilValue(isStartedAiProcessingState);
  const [isOpenProcessingLimit, setIsOpenProcessingLimit] = useRecoilState(isOpenProcessingLimitState);
  const setFindings = useSetRecoilState(findingsState);
  const setFacilityNote = useSetRecoilState(facilityNoteState);
  const setOriginalDailySummaries = useSetRecoilState(originalDailySummariesState);
  const setProfileAfibAvgHr = useSetRecoilState(profileAfibAvgHrState);
  const isShowTabHeader = useRecoilValue(isShowTabHeaderState);
  const setCombinedEventsInHeatmap = useSetRecoilState(combinedEventsInHeatmapState);
  const setEcgBookmarks = useSetRecoilState(ecgBookmarksState);
  const setActionSnapshotRecoil = useSetRecoilState(actionSnapshotRecoilState);
  const setHolterBeatEventsCount = useSetRecoilState(beatEventCountState);
  const setHolterRhythmEventsCount = useSetRecoilState(rhythmEventCountState);
  const setHolterOtherEventsCount = useSetRecoilState(otherEventCountState);
  const setHighPassThumbnail = useSetRecoilState(highPassThumbnailState);
  const setSizeEventThumbnailConfig = useSetRecoilState(sizeEventThumbnailConfigState);
  const setGainThumbnail = useSetRecoilState(gainThumbnailState);
  const setChannelsThumbnail = useSetRecoilState(channelsThumbnailState);
  const setReportOptions = useSetRecoilState(reportOptionsState);

  const isLoadedIndexDB = useRecoilValue(isLoadedIndexDBState);
  const [isUndoDisabled, setIsUndoDisabled] = useRecoilState(isUndoDisabledState);
  useEffect(() => {
    // Set the default value to true
    setTimeout(() => {
      setIsUndoDisabled(true);
    }, 100);
    const updateIndex = (currentIndex) => {
      setIsUndoDisabled(currentIndex < 1);
    };
    eventEmitter.addListener(EMITTER_CONSTANTS.UNDO_REDO_CURRENT_INDEX, updateIndex);

    // Clean up the event listener on component unmount
    return () => {
      eventEmitter.removeListener(EMITTER_CONSTANTS.UNDO_REDO_CURRENT_INDEX, updateIndex);
    };
  }, []);

  const {
    studyId,
    profileId,
    facilityId,
    reportId,
    isAbortedStudy,
    startReport,
    timezoneOffset,
  } = reportInfo;

  const roomReport = useRef();
  const updatingTabCount = useRef(0);
  const tabIndexConfirmNavigate = useRef();
  const tab1Ref = useRef();
  const tab2Ref = useRef();
  const tab3Ref = useRef();
  const tab4Ref = useRef();
  const findingRef = useRef();
  const ecgDisclosureRef = useRef();
  const sessionInfoRef = useRef();

  const fetchEventsCount = async () => {
    try {
      const eventsCountFilter = {
        studyId,
        profileId,
      };
      const promises = [
        fetchHolterBeatEventsCount(eventsCountFilter, false, null, false),
        fetchHolterRhythmEventsCount(eventsCountFilter, false, null, false),
        fetchHolterOtherEventsCount(eventsCountFilter, false, null, false),
      ];
      const [
        holterBeatEventsCount,
        holterRhythmEventsCount,
        holterOtherEventsCount,
      ] = await Promise.all(promises);
      setHolterBeatEventsCount(holterBeatEventsCount);
      setHolterRhythmEventsCount(holterRhythmEventsCount);
      setHolterOtherEventsCount(holterOtherEventsCount);
    } catch (error) {
      logError('Failed to fetch holter rhythm events count: ', error);
    }
  };

  const getDatabaseStateRecoil = async () => {
    const activeTab = await getDataStateRecoil('activeTab');
    if (activeTab?.value) {
      setState({ activeTab: activeTab?.value });
      defaultActiveTab.current = activeTab?.value;
    }
  };

  const restDefaultState = () => {
    setHighPassThumbnail(1);
    setGainThumbnail(7.5);
    setChannelsThumbnail([4]);
    setSizeEventThumbnailConfig({ name: 'Small', width: 415, height: 87.1 });
    setReportOptions(defaultReportOptions);
    upsertDataStateRecoil('hourlyBeatDataArray', []);
  };

  const clearDataAndResaveSessionInfo = async () => {
    await clearAllDataStateRecoil();
    setFindings(findingRef.current);
    if (!isNotEdit && sessionInfoRef.current) {
      upsertDataStateRecoil('sessionInfo', sessionInfoRef.current);
      upsertDataStateRecoil('activeTab', state.activeTab);
    }
  };

  const handleClearIndexDB = async (info) => {
    try {
      const dataSave = await getDataStateRecoil('sessionInfo');
      if (dataSave?.value) {
        const { studyId, profileId, sessionTime } = info;
        if (dataSave?.value?.studyId !== studyId
          || dataSave?.value?.profileId !== profileId
          || dayjs(dataSave?.value?.sessionTime).valueOf() !== dayjs(sessionTime).valueOf()) {
          // clean all data
          clearDataAndResaveSessionInfo();
          setIsLoadIndexDB(true);
        } else {
          // show popup when stay in tab 1 or tab 2
          const activeTab = await getDataStateRecoil('activeTab');
          // check has change data
          const isChanged = await checkHasChangedSavedIndexDB({
            activeTab: activeTab?.value,
            currentFinding: findingRef.current,
            currentEcgDisclosure: ecgDisclosureRef.current,
          });
          if (isChanged) {
            setState({ isOpenUseDbIndexModal: true });
          } else {
            clearDataAndResaveSessionInfo();
            setIsLoadIndexDB(true);
          }
        }
      } else {
        setIsLoadIndexDB(true);
      }
    } catch (error) {
      console.error(error);
    }
  };

  const onDiscardChangeSavedData = () => {
    setState({ isOpenUseDbIndexModal: false });
    clearDataAndResaveSessionInfo();
    setIsLoadIndexDB(true);
  };

  const onKeepEditingWithSavedData = () => {
    setState({ isOpenUseDbIndexModal: false });
    setIsLoadIndexDB(true);
  };

  const fetchReportData = async (studyFid) => {
    try {
      const input = {
        studyFid: studyFid ? parseInt(studyFid, 10) : undefined,
        type: REPORT_TYPE,
      };
      const aiReportData = await fetchNewReportData({ input });
      const { reportData, isSuccess } = aiReportData;
      if (isSuccess && !_.isEmpty(reportData)) {
        if (reportData.study?.isArchived || generateAiStatusReport(reportData.study) !== 'Ready') {
          history.replace('/404');
          return;
        }
        const studyId = reportData.study?.id;
        const profileId = reportData.study?.holterProfile?.id;
        const [downloadEcgDataMap, holterTasks, studyReportsData] = await Promise.all([
          fetchHolterEcgDataMap({ studyId, profileId }),
          fetchHolterTasks(),
          fetchNewReportData({ id: studyId, isStudyReport: true }),
        ]);
        if (downloadEcgDataMap) {
          CONFIG_ECG.samplingFrequency = downloadEcgDataMap.samplingFrequency;
          CONFIG_ECG.gain = downloadEcgDataMap.gain;
          ECG_VIEWER_CONFIG_STORE.visibleChannels = _.map(downloadEcgDataMap.channels, item => Number(item.slice(-1)) - 1);
          ECG_VIEWER_CONFIG_STORE.originalChannels = downloadEcgDataMap.channels;
        }
        const studyReport = _.filter(studyReportsData, x => x.type === 'Study')[0];
        const aiReport = _.find(studyReportsData, x => x.type === 'AI EOU');
        socketio.emitRoom(EMITTER_CONSTANTS.HOLTER_GENERAL_ROOM);
        socketio.emitRoom(EMITTER_CONSTANTS.HOLTER_PROFILE_ROOM.replace('{profileId}', profileId));
        socketio.emitRoom(reportData.study?.device?.deviceId);
        // *: Join room report
        if (reportData?.report?.type === 'Study' || reportData.report?.type === 'AI EOU' || reportData?.report?.reportId) {
          const room = `${zeroPad(studyFid)}_ai_eou`;
          roomReport.current = room;
          socketio.sendJoinReportRoom(roomReport.current);
        }
        // isHolterReport: [STUDY_TYPE.HOLTER, STUDY_TYPE.EXTENDED_HOLTER, STUDY_TYPE.MCT_PEEK].includes(reportData.study?.studyType)
        const startReport = reportData?.report?.start;
        const stopReport = reportData?.report?.stop;
        const timezone = reportData.study?.timezone;
        const timezoneOffset = timezoneToOffset(timezone);
        const listDate = generateListDate(startReport, stopReport, timezoneOffset);
        const combinedEventsInHeatmap = {};
        _.forEach(listDate, (date) => {
          combinedEventsInHeatmap[date] = [];
        });
        setCombinedEventsInHeatmap(combinedEventsInHeatmap);
        const info = {
          studyId: reportData.study?.id,
          profileId: reportData.study?.holterProfile?.id,
          sessionTime: reportData.study?.holterProfile?.sessionTime,
          facilityId: reportData.study?.facility?.id,
          reportId: reportData.report?.id,
          reportFid: reportData.report?.reportId,
          isAbortedStudy: reportData.study?.status === 'Aborted',
          studyFid: reportData.study?.friendlyId,
          patientName: `${reportData.study?.info?.patient?.firstName} ${reportData.study?.info?.patient?.lastName}`,
          facilityName: reportData.study?.facility?.name,
          reportType: reportData.report?.type,
          timezone,
          timezoneOffset,
          startReport,
          stopReport,
        };
        setReportInfo(info);

        // *: Sort data by start time
        downloadEcgDataMap.data.sort((a, b) => a.start - b.start);
        for (let i = 0; i < downloadEcgDataMap.data.length; i += 1) {
          const item = downloadEcgDataMap.data[i];
          item.start = dayjs(item.start);
          item.stop = dayjs(item.stop);
        }

        findingRef.current = reportData?.report?.technicianComments || reportData?.study?.holterProfile?.technicianComments;
        ecgDisclosureRef.current = {
          ecgDisclosure: reportData.study?.holterProfile?.ecgDisclosure,
          timezoneOffset,
          ecgDataMap: downloadEcgDataMap,
        };
        // check clean IndexDB
        handleClearIndexDB(info);

        logHandler.setStudyId(studyId);
        setProfileAfibAvgHr(reportData.study?.holterProfile?.afibAvgHr);
        setOriginalDailySummaries(reportData.study?.holterProfile?.mctDailySummaries);
        setEcgDataMap(downloadEcgDataMap);
        // setFindings(reportData?.report?.technicianComments || reportData?.study?.holterProfile?.technicianComments);
        setFacilityNote(reportData?.study?.facility?.facilityNote);
        setReportData(reportData);
        setIsOpenProcessingLimit(!isHolterProcessingAvailable(holterTasks, reportData?.study?.friendlyId));
        setState({
          isLoadedData: true,
          isGeneratingNormalReportInCallCenter: studyReport?.status === 'Rendering',
          studyReport,
          aiReport,
        });
      } else {
        history.replace('/404');
      }
    } catch (error) {
      logError('Failed to load report data ', error);
      history.push('/404');
    }
    loadingPageAction();
  };

  const toggleDisplayStudyInfoDrawer = () => {
    setState({ isOpenStudyInfoDrawer: !state.isOpenStudyInfoDrawer });
  };

  const toggleDisplayPatientInfoDrawer = () => {
    setState({ isOpenPatientInfoDrawer: !state.isOpenPatientInfoDrawer });
  };

  const toggleDisplayFacilityInfoDrawer = () => {
    setState({ isOpenFacilityInfoDrawer: !state.isOpenFacilityInfoDrawer });
  };

  const onClickReload = () => {
    // *Cannot use window.location.reload() because it not work in Firefox
    //  window.location.reload();
    window.location.href = window.location.href;
  };

  const handleOnClickArtifactReport = async (flag) => {
    try {
      await handleMarkReportAsArtifactReport(reportId, flag);
    } catch (err) {
      toastrError(err, t`Error`);
    }
    setState({ isOpenConfirmArtifact: false });
  };

  const navigateTab = (tab) => {
    if (tab !== state.activeTab) {
      if (isNotEdit) {
        setState({ activeTab: tab || defaultActiveTab.current });
        return;
      }

      // check data changed
      const tabRef = {
        1: tab1Ref.current,
        2: tab2Ref.current,
        3: tab3Ref.current,
        4: tab4Ref.current,
      }[state.activeTab];
      const isChangedData = tabRef ? tabRef.isChangedData() : false;
      console.log('[aiReport]-navigateTab', isChangedData, tab);
      if (isChangedData) {
        tabIndexConfirmNavigate.current = tab;
        setState({ isOpenConfirmNavigateTab: true });
      } else {
        //* Do not reset config
        // resetEcgViewerConfigStore();
        tabIndexConfirmNavigate.current = undefined;
        setState({ activeTab: tab || defaultActiveTab.current, isOpenConfirmNavigateTab: false });
        setActionSnapshotRecoil(1);
        // reset undo/redo related components
        restDefaultState();
        setTimeout(() => {
          resetHistoryStateRecoil();
        }, 500);
      }
    }
  };

  const onClickCancelNavigateTab = () => {
    tabIndexConfirmNavigate.current = undefined;
    setState({ isOpenConfirmNavigateTab: false });
  };

  const onClickNavigateWithoutSave = () => {
    const tabRef = {
      1: tab1Ref.current,
      2: tab2Ref.current,
      3: tab3Ref.current,
      4: tab4Ref.current,
    }[state.activeTab];
    if (tabRef) {
      tabRef.resetData();
    }
    const tabNavigate = tabIndexConfirmNavigate.current;
    // reset undo/redo related components
    restDefaultState();
    setTimeout(() => {
      resetHistoryStateRecoil();
    }, 500);
    setTimeout(() => {
      navigateTab(tabNavigate);
    }, 20);
  };

  const onClickConfirmNavigateTab = () => {
    const tabRef = {
      1: tab1Ref.current,
      2: tab2Ref.current,
      3: tab3Ref.current,
      4: tab4Ref.current,
    }[state.activeTab];
    if (tabRef) {
      tabRef.onClickSaveReport();
    }
    // reset undo/redo related components
    restDefaultState();
    setTimeout(() => {
      resetHistoryStateRecoil();
    }, 500);
  };

  const saveReportOnClick = () => {
    const tabRef = {
      1: tab1Ref.current,
      2: tab2Ref.current,
      3: tab3Ref.current,
      4: tab4Ref.current,
    }[state.activeTab];
    if (tabRef) {
      tabRef.onClickSaveReport();
    }
    // reset undo/redo related components
    restDefaultState();
    setTimeout(() => {
      resetHistoryStateRecoil();
    }, 500);
  };

  const handleNoActivity = () => {
    if (window.isStopAiProcess) {
      return;
    }

    socketio.sendLeaveAllRooms();
    setState({ isOpenNoActivityModal: true });
  };

  const checkDataChange = () => {
    const result = (state.isOpenNoActivityModal ? false : (
      tab1Ref.current?.isChangedData?.()
      || tab2Ref.current?.isChangedData?.()
      || tab3Ref.current?.isChangedData?.()
      || tab4Ref.current?.isChangedData?.()
    ));
    console.log('[aiReport]-checkStudyReportDataChange', result, state.isOpenNoActivityModal);
    return result;
  };

  const renderMarkArtifact = () => (
    <div id="bt-artifact">
      <IconButton
        disabled={
          reportData?.study?.holterProfile?.isArtifactReport
          || isNotEdit
        }
        iconComponent={(
          <img
            src={
              (reportData?.study?.holterProfile?.isArtifactReport || reportData?.report?.isArtifactReport)
                ? icArtifactActive
                : icArtifact
            }
            alt={t`Artifact icon`}
          />
        )}
        onClick={() => {
          if (!reportData?.report?.isArtifactReport) {
            setState({ isOpenConfirmArtifact: true });
          } else {
            handleOnClickArtifactReport(!reportData?.report?.isArtifactReport);
          }
        }}
      />

      <UncontrolledTooltip
        target="bt-artifact"
        className="custom-tooltip"
        placement="bottom"
        delay={{ show: 0, hide: 0 }}
        hideArrow
      >
        {(() => {
          if (reportData?.study?.holterProfile?.isArtifactReport) return t`Artifact report due to no beats and events detected`;
          if (isNotEdit) return IS_NOT_EDIT_REPORT_MESSAGE;
          return reportData?.report?.isArtifactReport ? t`Unmark report as artifact` : t`Mark report as artifact`;
        })()}
      </UncontrolledTooltip>
    </div>
  );

  const handleMarkReviewedClick = async () => {
    if (_.isEmpty(state.studyReport) && _.isEmpty(state.aiReport)) {
      return;
    }

    const isReviewedAiReport = state.aiReport.status === 'Reviewed';
    try {
      const status = state.aiReport.status === 'Reviewed'
        ? 'Ready'
        : 'Reviewed';
      const aiReportFilter = {
        id: state.aiReport.id,
        status,
      };
      let reportFilter;
      const promises = [handleUpdateReportStatusV2(aiReportFilter)];
      if (state.studyReport) {
        reportFilter = {
          id: state.studyReport.id,
          status,
        };
        promises.push(handleUpdateReportStatusV2(reportFilter));
      }
      await Promise.all(promises);
      setState({
        studyReport: state.studyReport
          ? {
            ...state.studyReport,
            status: reportFilter.status,
          }
          : null,
        aiReport: {
          ...state.aiReport,
          status: aiReportFilter.status,
        },
      });
      toastrSuccess(
        isReviewedAiReport
          ? t`Mark as unreviewed`
          : t`Mark as reviewed`,
        t`Success`,
      );
    } catch (error) {
      console.error('Failed to mark reviewed', error);
      toastrError(
        isReviewedAiReport
          ? t`Failed to mark as unreviewed`
          : t`Failed to mark as reviewed`,
        t`Error`,
      );
    }
  };

  const renderMarkReviewed = () => {
    // const isReviewedReport = state.studyReport?.status === 'Reviewed';
    const isReviewedAiReport = state.aiReport?.status === 'Reviewed';
    return (
      <div id="bt-mark-reviewed">
        <IconButton
          iconComponent={(
            <img
              src={
                isReviewedAiReport
                  ? ReviewedReportIcon
                  : UnreviewedReportIcon
              }
              alt={t`Artifact icon`}
            />
          )}
          MarkAsReviewedIcon
          onClick={handleMarkReviewedClick}
        />

        <UncontrolledTooltip
          target="bt-mark-reviewed"
          className="custom-tooltip"
          placement="bottom"
          delay={{ show: 0, hide: 0 }}
          hideArrow
        >
          {
            isReviewedAiReport
              ? t`Mark as unreviewed`
              : t`Mark as reviewed`
          }
        </UncontrolledTooltip>
      </div>
    );
  };

  const renderUndoState = () => (
    <div id="bt-undo-btn">
      <IconButton
        className="more-icon mr-3"
        isOutline
        disabled={isUndoDisabled}
        iconComponent={(
          <img src={UndoIcon} alt={t`Undo icon`} />
          )}
        onClick={() => { simulateKeyEvent('Z', { ctrlKey: true }); }}
      />

      <UncontrolledTooltip
        target="bt-undo-btn"
        className="custom-tooltip"
        placement="bottom"
        delay={{ show: 0, hide: 0 }}
        hideArrow
      >
        {/* TODO: currently set disabled is true */}
        {isUndoDisabled ? t`There is nothing to undo` : (isMacOS ? t`Undo  ⌘ Z` : t`Undo  Ctrl Z`)}
      </UncontrolledTooltip>
    </div>
  );

  const handleFetchBookmarks = async () => {
    try {
      const ecgBookmarks = await fetchListEcgBookmarks({
        studyId,
        sortOrder: 'asc',
        skip: 0,
      }, 0);
      setEcgBookmarks(ecgBookmarks);
    } catch (error) {
      logError('Failed to fetch bookmarks ', error);
    }
  };

  useEffect(() => {
    if (isStartedAiProcessing) {
      fetchEventsCount();
      handleFetchBookmarks();
    }
  }, [isStartedAiProcessing]);

  useEffect(() => {
    let interval;
    //* Refresh query signature every 1 hour
    if (state.isLoadedData) {
      const fetchNewQuerySignature = async () => {
        try {
          const querySignature = await fetchQuerySignature({ studyId });
          startTransition(() => {
            setEcgDataMap(currentEcgDataMap => ({ ...currentEcgDataMap, querySignature }));
          });
        } catch (error) {
          console.log(error);
        }
      };
      interval = setInterval(() => {
        fetchNewQuerySignature();
      }, 3600000); // 1 hour
    }
    return () => {
      try {
        if (interval) {
          clearInterval(interval);
        }
      } catch (error) {
        console.log(error);
      }
    };
  }, [state.isLoadedData, studyId]);

  useEffect(() => {
    if (Number(studyFid)) {
      // get database local
      getDatabaseStateRecoil();
      fetchReportData(studyFid);

      // TODO: Fetch other holter events types
      // handleFetchHolterOtherEventsTypes();
      // fetchHolterOtherEventsTypesAction();
    } else {
      history.replace('/404');
    }
  }, [studyFid]);

  useEffect(() => {
    upsertDataStateRecoil('activeTab', state.activeTab);
  }, [state.activeTab]);

  useEffect(() => {
    if (!isNotEdit) {
      const info = getReportInfo();
      const sessionTime = dayjs();
      // update session time
      const sessionInfo = {
        sessionTime: sessionTime.toISOString(),
        studyId: info.studyId,
        profileId: info.profileId,
      };
      const updateHolterProfileInput = {
        id: info.profileId,
        sessionTime,
      };
      sessionInfoRef.current = sessionInfo;
      handleUpdateHolterProfile(updateHolterProfileInput);
      upsertDataStateRecoil('sessionInfo', sessionInfo);
    }
  }, [isNotEdit]);

  useEmitter(EMITTER_CONSTANTS.REPORT_UPDATED, (msg) => {
    const reportDataClone = _.cloneDeep(reportData);
    if (msg.studyFid === reportDataClone.study?.friendlyId) {
      if (msg.id === reportDataClone.report?.id) {
        _.assign(reportDataClone.report, msg);
        setReportData(reportDataClone);
      } else {
        // Report format cũ ở bên callcenter nếu đang generate sẽ chặn ko cho copy report AI
        setState({ isGeneratingNormalReportInCallCenter: msg.status === 'Rendering' });
      }
    }
  }, [reportData]);

  useEmitter(EMITTER_CONSTANTS.REPORT_ROOM_USER, async (msg) => {
    const updatedEvent = _.findLast(msg, x => x.room === roomReport.current);
    if (updatedEvent) {
      const { users } = updatedEvent;

      const accessToken = auth.getOriginalToken();
      // *: Check user can edit report
      let newIsNotEdit = true;
      const sortedConflictUsers = users?.sort((a, b) => (a.time - b.time));
      const firstUser = sortedConflictUsers?.length > 0 ? sortedConflictUsers[0] : {};
      if (!_.isEmpty(firstUser.userId) && !_.isEmpty(firstUser.token)) {
        if (firstUser.userId === auth.userId() && firstUser.token === (accessToken || '').slice(-10)) {
          newIsNotEdit = false;
        } else {
          newIsNotEdit = true;
        }
      } else {
        newIsNotEdit = true;
      }
      console.log('[aiReport]-SOCKETROOMUSER', users, accessToken, firstUser, newIsNotEdit);
      //* Reload 2nd user when 1st user exit
      if (isNotEdit && !newIsNotEdit && isStartedAiProcessing && state.conflictUsers.length > 1 && users.length === 1) {
        logHandler.addLog('reload', {
          users, conflictUsers: state.conflictUsers, isNotEdit, newIsNotEdit,
        });
        await logHandler.sendLog();
        onClickReload();
        return;
      }
      setIsNotEdit(newIsNotEdit);
      setState({ conflictUsers: sortedConflictUsers });
    }
  }, [isNotEdit, state.conflictUsers, isStartedAiProcessing]);

  useEmitter(EMITTER_CONSTANTS.HOLTER_PROFILE_UPDATED, (msg) => {
    const reportDataClone = _.cloneDeep(reportData);
    if (msg?.ecgDisclosure) {
      _.assign(reportDataClone.study?.holterProfile, { ecgDisclosure: msg.ecgDisclosure });
    }
    if (!_.isEmpty(msg?.reportConfiguration)) {
      _.assign(reportDataClone.study?.holterProfile, { reportConfiguration: msg.reportConfiguration });
    }
    setReportData(reportDataClone);
  }, [reportData]);

  useEmitter(EMITTER_CONSTANTS.AI_UPDATE_TAB, (msg) => {
    updatingTabCount.current += 1;
    console.log('[aiReport]-AI_UPDATE_TAB', updatingTabCount.current);
  }, []);

  useEmitter(EMITTER_CONSTANTS.AI_LOADING, (msg) => {
    // {
    //   isLoading: boolean,
    // }
    const { isLoading } = msg || {};
    console.log('[aiReport]-AI_LOADING', msg, updatingTabCount.current, tabIndexConfirmNavigate.current, state.isOpenConfirmNavigateTab);
    if (isLoading) {
      updatingTabCount.current = 0;
      setState({ isLoadingSaveAiReport: true });
    } else if (updatingTabCount.current > 0) {
      updatingTabCount.current -= 1;
      if (updatingTabCount.current === 0) {
        setState({ isLoadingSaveAiReport: false });
        const tabNavigate = tabIndexConfirmNavigate.current;
        if (state.isOpenConfirmNavigateTab && tabNavigate) {
          setTimeout(() => {
            navigateTab(tabNavigate);
          }, 20);
        }
      }
    } else {
      updatingTabCount.current = 0;
      setState({ isLoadingSaveAiReport: false });
      const tabNavigate = tabIndexConfirmNavigate.current;
      if (state.isOpenConfirmNavigateTab && tabNavigate) {
        setTimeout(() => {
          navigateTab(tabNavigate);
        }, 20);
      }
    }
  }, [state.isOpenConfirmNavigateTab]);

  useEmitter(EMITTER_CONSTANTS.COMMAND_PROGRESS_UPDATED, (msg) => {
    const {
      command, doneTasks, totalTasks, percentage,
    } = msg;
    if (command === 'update-report-data') {
      loadingPageAction(`${LOADING_TYPES.GENERATE_REPORT} ${percentage} %`);
    }
  }, []);

  useEmitter(EMITTER_CONSTANTS.EVENTSUPDATED_EVENT, (msg) => {
    if (isStartedAiProcessing) {
      fetchEventsCount();
    }
  }, [isStartedAiProcessing]);

// Function to group data by day
const groupByDay = (studySummaries) => {
  return studySummaries.reduce((acc, summary) => {
    const date = dayjs(summary.start).format('YYYY-MM-DD');
    if (!acc[date]) {
      acc[date] = [];
    }
    acc[date].push(summary);
    return acc;
  }, {});
};


const handleDownloadBeatList = async (studySummariesJson) => {
  let thread;

  try {
    await openDBBeatData();
    await openDBEcgData();

    const worker = new Worker(beatWorker);
    thread = await spawn(worker);

    // Group the study summaries by day
    const groupedByDay = groupByDay(studySummariesJson);

    // Loop through each day group and process them
    for (const day in groupedByDay) {
      debugger;
      const daySummaries = groupedByDay[day];

      // Configure the thread with necessary parameters for each day
      await thread.createConfig({
        ecgDataMap,
        samplingFrequency: ecgDataMap.samplingFrequency,
      });

      worker.onmessage = async (message) => {
        const { type, data } = message.data;

        if (type === 'beatData') {
          const { result, metadata } = data;
          console.log('[beatWorker] beat', result.length, metadata);
          await addMultipleBeatData(result);
          if (metadata) {
            await addMetaBeatData(metadata.dateTime, metadata);
          }
        } else if (type === 'ecgData') {
          const { result, metadata } = data;
          console.log('[beatWorker] ecgData', result.length, metadata);
          await addMultipleECGData(result);
          if (metadata) {
            await addMetaEcgData(metadata.dateTime, metadata);
          }
        }
      };

      await thread.downloadBeatListByDay(daySummaries);
      await thread.downloadECGListByDay();
    }

    // Send post message when done
    postMessage({ status: 'completed' });

  } catch (error) {
    console.error('Failed to download beat list:', error);
  } finally {
    if (thread) {
      Thread.terminate(thread).catch(console.error);
    }
  }
};

  useEffect(() => {
    if (
      ecgDataMap
      && Object.keys(ecgDataMap).length > 0
      && !_.isEqual(ecgDataMap.data.length, prevEcgDataMap?.data.length)
    ) {
      // Call the function with your study summaries JSON data
      handleDownloadBeatList(studySummariesJson);
      setPrevEcgDataMap(ecgDataMap);
    }
  }, [ecgDataMap]);


  return (
    <>
      <CheckSavePrompt
        isNotEdit={isNotEdit}
        condition={checkDataChange}
      />
      <StudyInfoDrawer
        visible={state.isOpenStudyInfoDrawer}
        title={t`Study Information`}
        fetchStudyId={studyId}
        handleClickCloseButton={toggleDisplayStudyInfoDrawer}
      />
      <PatientInfoDrawer
        visible={state.isOpenPatientInfoDrawer}
        title={t`Patient Information`}
        studyId={studyId}
        handleClickCloseButton={toggleDisplayPatientInfoDrawer}
      />
      <FacilityInfoDrawer
        visible={state.isOpenFacilityInfoDrawer}
        title={t`Facility Information`}
        facilityId={facilityId}
        handleClickCloseButton={toggleDisplayFacilityInfoDrawer}
        isAbortedStudy={isAbortedStudy}
      />
      <ConfirmModal
        isOpen={isOpenProcessingLimit}
        onClickRightButton={() => { document.location.href = CALL_CENTER_PORTAL_URL; }}
        rightButtonName={t`Okay`}
        title={t`Approaching processing limit`}
        question={t`The current limit for viewing and processing reports by AI has been exceeded.
          Please try again later.`}
        isRedBtn={false}
      />
      <ConfirmModal
        isOpen={state.isOpenNoActivityModal}
        onClickRightButton={onClickReload}
        rightButtonName={t`Reload The Page`}
        title={t`Notification`}
        question={t`Since there has been no activity for the past 30 minutes, please reload the page to resume.`}
        isRedBtn={false}
      />
      <ConfirmModal
        isOpen={state.isOpenConfirmArtifact}
        onClickLeftButton={() => { setState({ isOpenConfirmArtifact: false }); }}
        onClickRightButton={() => handleOnClickArtifactReport(true)}
        leftButtonName={t`No`}
        rightButtonName={t`Yes`}
        title={t`Mark report as artifact`}
        question={t`Are you sure you want to mark this report as an "Artifact report"?`}
      />
      <ConfirmModal
        isOpen={state.isOpenUseDbIndexModal}
        className="unsaved-change-modal --use-db-index"
        onClickLeftButton={onDiscardChangeSavedData}
        onClickRightButton={onKeepEditingWithSavedData}
        leftButtonName={t`Discard changes`}
        rightButtonName={t`Keep editing`}
        title={t`Unsaved changes`}
        question={t`There are unsaved changes. If you would like to keep changes, press the “Keep editing” button below.`}
      />
      <ConfirmModal
        isOpen={state.isOpenConfirmNavigateTab}
        className="unsaved-change-modal"
        onClickLeftButton={onClickNavigateWithoutSave}
        onClickRightButton={onClickConfirmNavigateTab}
        leftButtonName={state.isLoadingSaveAiReport ? null : t`Don’t Save`}
        rightButtonName={t`Save Changes`}
        isFilledRightBtn
        isBoldLeftBtn
        title={(
          <div className="title-unsaved-change">
            <div>{t`Unsaved changes`}</div>
            <IconButton
              iconComponent={(
                <img
                  src={closeIcon}
                  alt={t`Close icon`}
                />
              )}
              onClick={onClickCancelNavigateTab}
            />
          </div>
        )}
        question={t`You're about to leave this tab with unsaved changes. What would you like to do before proceeding?`}
        isRedBtn={false}
        isSaving={state.isLoadingSaveAiReport}
      />
      <div
        className={classnames('custom-page')}
      >
        <ReportHeaderWrapper
          isLoadedData={state.isLoadedData}
          title={t`End of Use report`}
          conflictUsers={state.conflictUsers}
          toggleDisplayStudyInfoDrawer={toggleDisplayStudyInfoDrawer}
          toggleDisplayPatientInfoDrawer={toggleDisplayPatientInfoDrawer}
          toggleDisplayFacilityInfoDrawer={toggleDisplayFacilityInfoDrawer}
          markArtifactComponent={renderMarkArtifact()}
          markReviewedComponent={renderMarkReviewed()}
          undoStateComponent={renderUndoState()}
          isDisableSaveReport={isNotEdit || isAbortedStudy}
          onClickSaveReport={saveReportOnClick}
          isLoadingSaveAiReport={state.isLoadingSaveAiReport}
          isNotEdit={isNotEdit}
          profileId={profileId}
          studyId={studyId}
          isArtifactReport={reportData?.report?.isArtifactReport}
          activeTab={state.activeTab}
          navigateTab={navigateTab}
          isStartedAiProcessing={isStartedAiProcessing}
        />
        {
          state.isLoadedData && (
            <>
              {
                isLoadIndexDB && (
                  <InitRecoilFromIndexDB />
                )
              }
              {/* component undo and redo */}
              <UndoStateRecoil />

              <DebugObserver tab1Ref={tab1Ref} tab2Ref={tab2Ref} tab3Ref={tab3Ref} tab4Ref={tab4Ref} />
              <TabContent
                className={classnames('study-report-top-tab-content', isShowTabHeader ? '' : '--hide-tab')}
                activeTab={state.activeTab}
              >
                <TabPane tabId="1">
                  <BeatHR
                    tabRef={tab1Ref}
                    handleNoActivity={handleNoActivity}
                    activeTab={state.activeTab}
                  />
                </TabPane>

                <TabPane tabId="2">
                  <RhythmEvents
                    tabRef={tab2Ref}
                    handleNoActivity={handleNoActivity}
                    activeTab={state.activeTab}
                  />
                </TabPane>

                <TabPane tabId="3">
                  {isLoadedIndexDB ? (
                    <StripsManagement
                      ref={tab3Ref}
                      isActive={state.activeTab === '3'}
                      ecgDataMap={ecgDataMap}
                      reportData={reportData}
                      studyFid={parseInt(studyFid, 10)}
                      timezoneOffset={timezoneOffset}
                      startReport={startReport}
                      isNotEdit={isNotEdit}
                      reportType={REPORT_TYPE}
                    />
                  ) : <ProgressLoading />}
                </TabPane>

                <TabPane tabId="4">
                  {isLoadedIndexDB ? (
                    <AiPDFReportTab
                      ref={tab4Ref}
                      isActive={state.activeTab === '4'}
                      isAbortedStudy={isAbortedStudy}
                      isNotEdit={isNotEdit}
                      isGeneratingNormalReportInCallCenter={state.isGeneratingNormalReportInCallCenter}
                    />
                  ) : <ProgressLoading />}

                </TabPane>
              </TabContent>
            </>
          )
        }
      </div>
    </>
  );
};

AiReportPage.propTypes = {

};

export default AiReportPage;
The Fetch API is a built-in JavaScript interface to make network requests to a server. It has a fetch() method you can use to make GET, POST, PUT, or PATCH requests. In this project, you'll make a GET request to a URL for a JSON file with information about authors on freeCodeCamp News.

Here is how you can make a GET request with the fetch() method:

Example Code
fetch("url-goes-here")

The fetch() method returns a Promise, which is a placeholder object that will either be fulfilled if your request is successful, or rejected if your request is unsuccessful.

If the Promise is fulfilled, it resolves to a Response object, and you can use the .then() method to access the Response.

Here's how you can chain .then() to the fetch() method:

Example Code
fetch("sample-url-goes-here")
  .then((res) => res)

The data you get from a GET request is not usable at first. To make the data usable, you can use the .json() method on the Response object to parse it into JSON. If you expand the Prototype of the Response object in the console, you will see the .json() method there.

In order to start working with the data, you will need to use another .then() method.

Chain another .then() to the existing .then() method. This time, pass in data as the parameter for the callback function. For the callback, use a curly brace because you will have more than one expression.

The .catch() method is another asynchronous JavaScript method you can use to handle errors. This is useful in case the Promise gets rejected.
<style>
.content-hide {
    display: none;
}

.details-area {
    max-height: 70px;
    overflow: hidden;
    transition: max-height 0.5s ease;
}

.show-more, .show-less {
cursor: pointer;
}

</style>

<script type="text/javascript">
var $ = jQuery;
$(document).ready(function() {
    $('.show-more').on('click', function(event) {
        event.preventDefault();
        var profileCard = $(this).closest('.profile-card');
        var detailsArea = profileCard.find('.details-area');
        
        $(this).addClass('content-hide');
        profileCard.find('.show-less').removeClass('content-hide');

        detailsArea.css('max-height', detailsArea[0].scrollHeight + 'px'); // Rozwijanie
    });

    $('.show-less').on('click', function(event) {
        event.preventDefault();
        var profileCard = $(this).closest('.profile-card');
        var detailsArea = profileCard.find('.details-area');
        
        $(this).addClass('content-hide');
        profileCard.find('.show-more').removeClass('content-hide');
        
        detailsArea.css('max-height', '70px'); // Zwijanie
    });
});
</script>
    function showPreview(event){
  if(event.target.files.length > 0){
    var src = URL.createObjectURL(event.target.files[0]);
    var preview = document.getElementById("file-ip-1-preview");
    preview.src = src;
    preview.style.display = "block";
  }
}
import classnames from 'classnames';
import _, { isEqual } from 'lodash';
import React, {
  useCallback,
  useEffect,
  useRef,
  useState,
} from 'react';
import {
  useRecoilState,
  useRecoilValue,
  useSetRecoilState,
} from 'recoil';
import { t } from 'ttag';
import fetchHolterRrCellsBeats from '../../../../../Apollo/Functions/fetchHolterRrCellsBeats';
import SelectionAreaWrapper from '../../../../../ComponentsV2/SelectionAreaWrapper';
import { EMITTER_CONSTANTS } from '../../../../../ConstantsV2';
import {
  KEY_CANCEL,
  KEY_RECOIL,
  STRIP_BEAT_THUMBNAIL_INFO,
} from '../../../../../ConstantsV2/aiConstants';
import selectCellHeatmapIcon from '../../../../../StaticV2/Images/Components/select-cell-heatmap-icon.svg';
import storeTimeout from '../../../../../Store';
import {
  useGetRecoilValue,
  useUpdateEffect,
} from '../../../../../UtilsV2/customHooks';
import emitter from '../../../../../UtilsV2/eventEmitter';
import { toastrError } from '../../../../../UtilsV2/toastNotification';
import {
  activeTabState,
  beatChangesState,
  channelsThumbnailState,
  gainThumbnailState,
  highPassThumbnailState,
  isLoadingStripState,
  pageIndexState,
  selectedMultipleStripState,
  selectedStripState,
  sizeBeatThumbnailConfigState,
} from '../../Recoil';
import {
  BEAT_HR_TAB_ID,
  getStripIndex,
  getStripPosition,
  logError,
} from '../../handler';
import {
  ID_DIV_STRIP_BEATS,
  formatBeatStripRr,
  getListBeatTime,
  handleCacheListHolterBeats,
  handleDownloadData,
  updateMultipleStripType,
} from '../helper';
import { processEcgData } from '../../../../../UtilsV2/utils/ecghandle';

import {
  bulkActionState,
  listSelectedCellsState,
  loadingBeatStatusState,
  totalStripState,
} from '../recoil';
import {
  BULK_ACTION_ENUM,
  LOAD_BEAT_STATUS,
} from '../recoil/model';
import BeatPreviewStrip from './beatPreviewStrip';

const BEAT_PREVIEW_STRIP_OFFSET = 2000;

const cache = new Map();

const StripBeats = (props) => {
  const keyRecoil = KEY_RECOIL.TAB_1;
  const [isLoadingStrip, setIsLoadingStrip] = useRecoilState(isLoadingStripState(keyRecoil));
  const [selectedStrip, setSelectedStrip] = useRecoilState(selectedStripState(keyRecoil));
  const [loadingBeatStatus, setLoadingBeatStatus] = useRecoilState(loadingBeatStatusState);
  const activeButton = useRecoilValue(activeTabState(keyRecoil));
  const pageIndex = useRecoilValue(pageIndexState(keyRecoil));
  const sizeThumbnailConfig = useRecoilValue(sizeBeatThumbnailConfigState);
  const channelsThumbnail = useRecoilValue(channelsThumbnailState);
  const gainThumbnail = useRecoilValue(gainThumbnailState);
  const highPassThumbnail = useRecoilValue(highPassThumbnailState);
  const bulkAction = useRecoilValue(bulkActionState);
  const getBulkAction = useGetRecoilValue(bulkActionState);
  const listSelectedCells = useRecoilValue(listSelectedCellsState);
  const setSelectedMultipleStrip = useSetRecoilState(selectedMultipleStripState(keyRecoil));
  const setTotalStrip = useSetRecoilState(totalStripState);
  const getListSelectedCells = useGetRecoilValue(listSelectedCellsState);
  const setPageIndex = useSetRecoilState(pageIndexState(keyRecoil));
  const getBeatChanges = useGetRecoilValue(beatChangesState(keyRecoil));
  const setBeatChanges = useSetRecoilState(beatChangesState(keyRecoil));

  const [holterBeats, setHolterBeats] = useState([]);
  const stripPosition = useRef({ x: 0, y: 0 });
  const prevPageIndex = useRef(pageIndex.index);
  const lastCurrentCellsHighestPage = useRef(0);
  const arrowKeyToPrevPage = useRef(false);
  const selectionAreaWrapperRef = useRef();
  const timeoutRef = useRef();

  let isActiveQuery;

  // Handle drag select
  const setSelectedBeat = useCallback((selected) => {
    //* Convert Set to array
    const selectedArr = [...selected];
    const selectedBeats = [];
    _.forEach(selectedArr, (item) => {
      const id = item.split('-')[1];
      const selectedBeat = (holterBeats || []).find(beat => beat.id === Number(id));
      if (selectedBeat) {
        selectedBeats.push(selectedBeat);
      }
    });
    setSelectedMultipleStrip((prev) => {
      if (prev.length === 0 && selectedBeats.length === 0) {
        return prev;
      }
      return selectedBeats;
    });
  }, [holterBeats]);

  const fetchBackgroundMoreBeatRr = (index, rrCellIds, waitTime = 0, stopCall = false) => {
    const timerId = setTimeout(async () => {
      storeTimeout.removeStore(activeButton, timerId);
      const filter = {
        studyId: props.studyId,
        profileId: props.profileId,
        rrHeatMapType: activeButton,
        rrCellIds,
        skip: index * STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit,
      };
      const {
        beats,
        hesBeatStatus,
        beatChannels,
        totalBeats,
      } = await fetchHolterRrCellsBeats(filter, STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit, KEY_CANCEL.API_HOLTER_AI);
      if (totalBeats > (index + 1) * STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit && !stopCall) {
        fetchBackgroundMoreBeatRr(index + 1, rrCellIds, 0, true);
      }
      if (beats.length > 1) {
        const timeRanges = _.map(beats, epoch => ({
          start: new Date(epoch - 5000).toISOString(),
          stop: new Date(epoch + 5000).toISOString(),
        }));
        handleCacheListHolterBeats(props.studyId, props.profileId, timeRanges);
      }
      const data = formatBeatStripRr(beats, hesBeatStatus, beatChannels, props.ecgDataMap.samplingFrequency);
      _.forEach(data, (d) => {
        processEcgData({
          ecgDataMap: props.ecgDataMap,
          beat: d,
          offset: BEAT_PREVIEW_STRIP_OFFSET,
          dataLength: props.ecgDataMap.samplingFrequency * 2,
        });
      });
      cache.set(index, data); // Cache the fetched data
    }, waitTime);
    timeoutRef.current = timerId;
    storeTimeout.pushStore(activeButton, timerId);
  };

  const handleChangeBulkBeatType = ({ type, strips = [] }) => {
    const stripChangesUpdate = updateMultipleStripType({
      stripChanges: getBeatChanges(),
      strips,
      type,
      isEvent: false,
    });
    setBeatChanges(stripChangesUpdate);
  };

  const fetchBeats = async ({ rrCellIds }) => {
    setIsLoadingStrip(true);
    try {
      const validatePage = pageIndex.index <= 0 ? 0 : pageIndex.index;
      if (cache.has(validatePage)) {
        // Load from cache
        const cachedData = cache.get(validatePage);
        setHolterBeats(cachedData);
        setIsLoadingStrip(false);
      } else {
        const filter = {
          studyId: props.studyId,
          profileId: props.profileId,
          rrHeatMapType: activeButton,
          rrCellIds,
          skip: validatePage * STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit,
        };
        const {
          beats,
          hesBeatStatus,
          beatChannels,
          totalBeats,
        } = await fetchHolterRrCellsBeats(filter, STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit, KEY_CANCEL.API_HOLTER_AI);
        const listDate = getListBeatTime(beats, props.timezoneOffset);

        //* Fetch more beats via worker
        if (totalBeats > (validatePage + 1) * STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit) {
          fetchBackgroundMoreBeatRr(validatePage + 1, rrCellIds);
        }

        if (beats.length > 1) {
          const timeRanges = _.reduce(beats, (result, epoch, index) => {
            if (index > 0) {
              result.push({
                start: new Date(epoch - 5000).toISOString(),
                stop: new Date(epoch + 5000).toISOString(),
              });
            }
            return result;
          }, []);
          handleCacheListHolterBeats(props.studyId, props.profileId, timeRanges);
        }

        const data = formatBeatStripRr(beats, hesBeatStatus, beatChannels, props.ecgDataMap.samplingFrequency);
        cache.set(validatePage, data); // Cache the fetched data
        props.callbackListDate(listDate);
        //* Arrow button to prev page
        if (validatePage < prevPageIndex.current && arrowKeyToPrevPage.current) {
          stripPosition.current = getStripPosition(data.length - 1, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          setSelectedStrip(data[data.length - 1]);
        } else {
          const selectedStripIndex = _.findIndex(data, x => x.id === selectedStrip?.id);
          const tempSelectedStrip = selectedStripIndex === -1 ? (data[0] || null) : data[selectedStripIndex];
          stripPosition.current = getStripPosition(selectedStripIndex === -1 ? 0 : selectedStripIndex, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          setSelectedStrip(tempSelectedStrip);
        }
        const bulkAction = getBulkAction();
        //* Khi bulk action thì next page mới auto change, nếu back về và set bulk action thì chỉ apply cho page đó
        if (bulkAction !== BULK_ACTION_ENUM.NOTHING && validatePage > lastCurrentCellsHighestPage.current) {
          handleChangeBulkBeatType({
            type: bulkAction,
            strips: data,
          });
          lastCurrentCellsHighestPage.current = validatePage > lastCurrentCellsHighestPage.current ? validatePage : lastCurrentCellsHighestPage.current;
        }
        arrowKeyToPrevPage.current = false;
        prevPageIndex.current = validatePage;
        setTotalStrip(totalBeats ?? 0);
        setHolterBeats(data);
        setIsLoadingStrip(false);
      }
    } catch (error) {
      logError('Failed to fetch rr heatmap beats: ', error);
      toastrError(error.message, 'Error');
    }
  };

  const handleClickSelectBeat = useCallback((beat, index) => {
    setSelectedStrip((prev) => {
      if (beat?.id !== prev?.id) {
        stripPosition.current = getStripPosition(index, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
        return beat;
      }
      return prev;
    });
  }, []);

  const cacheNextPages = async (rrCellIds, currentPage) => {
    for (let i = currentPage + 1; i <= currentPage + 5; i++) {
      if (!cache.has(i)) {
        fetchBackgroundMoreBeatRr(i, rrCellIds, 0, true);
      }
    }
  };

  useUpdateEffect(() => {
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.FETCH) {
      const { x: row, y: column } = stripPosition.current;
      const currentStripIndexInList = column + row * STRIP_BEAT_THUMBNAIL_INFO.prevStripPerRow;
      const currentBeatIndex = currentStripIndexInList + pageIndex.index * STRIP_BEAT_THUMBNAIL_INFO.prevStripDisplayLimit;
      const newPage = Math.floor(currentBeatIndex / STRIP_BEAT_THUMBNAIL_INFO.stripDisplayLimit);
      setPageIndex({ index: newPage < 0 ? 0 : newPage });
    }
  }, [sizeThumbnailConfig]);

  useEffect(() => {
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.NONE) {
      setIsLoadingStrip(false);
      setHolterBeats([]);
    }
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.FETCH) {
      isActiveQuery = true;
      const listSelectedCells = _.filter(getListSelectedCells(), x => !_.isNaN(Number(x.id)));
      const rrCellIds = _.map(listSelectedCells, x => x.id);
      if (rrCellIds.length !== 0) {
        fetchBeats({ rrCellIds });
        // cacheNextPages(rrCellIds, pageIndex.index);
      }
    }
    if (loadingBeatStatus.status === LOAD_BEAT_STATUS.RELOAD) {
      isActiveQuery = true;
      const listSelectedCells = _.filter(getListSelectedCells(), x => !_.isNaN(Number(x.id)));
      const rrCellIds = _.map(listSelectedCells, x => x.id);
      if (rrCellIds.length !== 0 && selectedStrip?.id) {
        setLoadingBeatStatus({ status: LOAD_BEAT_STATUS.FETCH });
        setPageIndex({ index: 0 });
      }
    }
    return () => {
      isActiveQuery = false;
    };
  }, [loadingBeatStatus, pageIndex]);

  useEffect(() => {
    lastCurrentCellsHighestPage.current = 0;
  }, [listSelectedCells]);

  useUpdateEffect(() => {
    if (bulkAction !== BULK_ACTION_ENUM.NOTHING && !isLoadingStrip) {
      handleChangeBulkBeatType({
        type: bulkAction,
        strips: holterBeats,
      });
      const validatePage = pageIndex.index <= 0 ? 0 : pageIndex.index;
      lastCurrentCellsHighestPage.current = validatePage > lastCurrentCellsHighestPage.current ? validatePage : lastCurrentCellsHighestPage.current;
    }
  }, [bulkAction]);

  useEffect(() => {
    const handleKeyDown = (event) => {
      if (_.isEmpty(selectionAreaWrapperRef.current) || event?.repeat || isLoadingStrip) {
        return;
      }
      const tabZone = document.getElementById(BEAT_HR_TAB_ID);
      // *:Active key press when hover in tab zone
      if (tabZone && !tabZone.matches(':hover')) {
        return;
      }
      event.preventDefault();
      const { key } = event;
      switch (key) {
        case 'ArrowRight': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.y += 1;
          const index = getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          if (index > holterBeats.length - 1) {
            stripPosition.current.y -= 1;
            emitter.emit(EMITTER_CONSTANTS.BEAT_HR_NEXT);
          } else {
            const beat = holterBeats[index];
            if (beat) {
              setSelectedStrip(beat);
            } else {
              stripPosition.current.y -= 1;
            }
          }
          break;
        }
        case 'ArrowLeft': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.y -= 1;
          const index = getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow);
          if (index < 0) {
            arrowKeyToPrevPage.current = true;
            stripPosition.current.y += 1;
            emitter.emit(EMITTER_CONSTANTS.BEAT_HR_PREV);
          } else {
            const beat = holterBeats[index];
            if (beat) {
              setSelectedStrip(beat);
            } else {
              stripPosition.current.y += 1;
            }
          }
          break;
        }
        case 'ArrowUp': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.x -= 1;
          const beat = holterBeats[getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow)];
          if (beat) {
            setSelectedStrip(beat);
          } else {
            stripPosition.current.x += 1;
          }
          break;
        }
        case 'ArrowDown': {
          selectionAreaWrapperRef.current.resetSelection();
          stripPosition.current.x += 1;
          const beat = holterBeats[getStripIndex(stripPosition.current, STRIP_BEAT_THUMBNAIL_INFO.stripPerRow)];
          if (beat) {
            setSelectedStrip(beat);
          } else {
            stripPosition.current.x -= 1;
          }
          break;
        }
        default:
      }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [holterBeats]);

  useEffect(() => () => {
    storeTimeout.removeStore(activeButton, timeoutRef.current);
    cache.clear();
  }, [activeButton]);

  if (loadingBeatStatus.status === LOAD_BEAT_STATUS.NONE) {
    return (
      <div id={ID_DIV_STRIP_BEATS} className="no-cell-selected">
        <img src={selectCellHeatmapIcon} alt="select cell on heat map" />
        {t`Select a cell on the heatmap chart below to view its beats.`}
      </div>
    );
  }

  return (
    <SelectionAreaWrapper
      ref={selectionAreaWrapperRef}
      id={ID_DIV_STRIP_BEATS}
      className="strips-container"
      selectables=".single-strip-beat"
      handleSetSelected={setSelectedBeat}
      defaultSelectedId={selectedStrip?.id}
      isNoData={holterBeats?.length === 0}
      isLoading={isLoadingStrip}
    >
      {
        selected => (
          <div className={classnames('strips-img-container')}>
            {
              _.map(holterBeats, (beat, index) => (
                <BeatPreviewStrip
                  key={beat.id}
                  index={index}
                  id={`beat-${beat.id}`}
                  isActive={beat?.id === selectedStrip?.id}
                  isSelected={selected.has(`beat-${beat.id}`)}
                  beat={beat}
                  gain={gainThumbnail}
                  channels={channelsThumbnail}
                  width={sizeThumbnailConfig.width}
                  height={sizeThumbnailConfig.height}
                  ecgDataMap={props.ecgDataMap}
                  highPass={highPassThumbnail}
                  offset={BEAT_PREVIEW_STRIP_OFFSET}
                  studyId={props.studyId}
                  profileId={props.profileId}
                  onClickSelectBeat={handleClickSelectBeat}
                  activeButton={activeButton}
                />
              ))
            }
          </div>
        )
      }
    </SelectionAreaWrapper>
  );
};

export default StripBeats;
import axios from 'axios';
import _ from 'lodash';
import { expose } from 'threads/worker';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';

dayjs.extend(isBetween); // Use the isBetween plugin for dayjs
const config = {};

const downloadFile = async (url) => {
  try {
    const response = await axios.get(url, { responseType: 'arraybuffer' });
    let { data } = response;
    if (data.byteLength % 5 !== 0) {
      data = data.slice(0, data.byteLength - (data.byteLength % 5));
    }
    return data;
  } catch (error) {
    console.error('Failed to download file:', error);
    return [];
  }
};

const processChunkedArray = (chunkedArray, start, stop) => {
  const beatData = {
    start,
    stop,
    beatPositions: [],
    beatEpochs: [],
    hesBeatStatus: [],
  };

  chunkedArray.forEach((item) => {
    const beatBytes = item.slice(2, 5).map(byte => (byte < 0 ? 256 + byte : byte));
    const beatPosition = (beatBytes[0] << 16) + (beatBytes[1] << 8) + beatBytes[2];
    const beatEpoch = dayjs(start).add(beatPosition / config.samplingFrequency, 'second').valueOf();
    beatData.beatPositions.push(beatPosition);
    beatData.beatEpochs.push(beatEpoch);
    beatData.hesBeatStatus.push(item[1]);
  });

  return beatData;
};

const processBeatData = (data, start, stop) => {
  const array = new Int8Array(data);
  const chunkedArray = _.chunk(array, 5);
  return processChunkedArray(chunkedArray, start, stop);
};

const trackedPromise = async (url, key, processFunction) => {
  try {
    const response = await axios.get(url, { responseType: 'arraybuffer' });
    let { data } = response;
    if (data.byteLength % 5 !== 0) {
      data = data.slice(0, data.byteLength - (data.byteLength % 5));
    }
    return { key, value: processFunction(data) };
  } catch (error) {
    console.error('Failed to download file:', error);
    return { key, value: [] };
  }
};

expose({
  createConfig: ({ samplingFrequency, ecgDataMap }) => {
    config.samplingFrequency = samplingFrequency;
    config.ecgDataMap = ecgDataMap;
  },

  downloadBeatListByDay: async (studySummariesJson) => {
    try {
      const pendingDownloads = [];
      const results = [];
      let completed = 0;
      const total = studySummariesJson.length;

      const incrementCompleted = () => {
        completed += 1;
        const status = (completed / total) * 100;
        postMessage({ type: 'progress', data: { status } });
      };

      for (const [i, fileUrl] of studySummariesJson.entries()) {
        const { start, stop } = studySummariesJson[i];
        const keyStart = dayjs(start).startOf('hour').valueOf();
        const fileUrlPath = fileUrl?.aiPredict?.beatFinalPath;

        const promise = trackedPromise(fileUrlPath, keyStart, (data) => processBeatData(data, start, stop))
          .then((result) => {
            results.push(result);
            incrementCompleted();
          })
          .catch((error) => {
            console.error('Failed to download beat list:', error);
            incrementCompleted();
          });

        pendingDownloads.push(promise);
      }

      await Promise.all(pendingDownloads);
      postMessage({ type: 'beatData', data: results });
      return 'done-beat';
    } catch (error) {
      console.error('Failed to download beat list:', error);
      return [];
    }
  },

  downloadECGListByDay: async () => {
    try {
      const pendingDownloads = [];
      const { ecgDataMap } = config;
      let completed = 0;
      const total = ecgDataMap.data.length;
      const results = [];

      const incrementCompleted = () => {
        completed += 1;
        const status = (completed / total) * 100;
        postMessage({ type: 'progress', data: { status } });
      };

      for (const itemECG of ecgDataMap.data) {
        const fileUrl = `${itemECG.dataUrl}?${ecgDataMap.querySignature}`;
        const key = dayjs(itemECG.start.$d).startOf('hour').valueOf();

        const promise = trackedPromise(fileUrl, key, (data) => data)
          .then((result) => {
            results.push(result);
            incrementCompleted();
          })
          .catch((error) => {
            console.error('Failed to download ECG list:', error);
            incrementCompleted();
          });

        pendingDownloads.push(promise);
      }

      await Promise.all(pendingDownloads);
      postMessage({ type: 'ecgData', data: results });
      return { result: results, status: 100 };
    } catch (error) {
      console.error('Failed to download ECG list:', error);
      return { result: [], status: 0 };
    }
  },
});
<!DOCTYPE html>
<html>
​
<body>
  
<script>
class Car {
  constructor(name) {
    this.brand = name;
  }
​
  present() {
    return 'I have a ' + this.brand;
  }
}
​
class Model extends Car {
  constructor(name, mod) {
    super(name);
    this.model = mod;
  }  
  show() {
    return this.present() + ', it is a ' + this.model
  }
}
​
const mycar = new Model("Ford", "Mustang");
document.write(mycar.show());
</script>
​
</body>
</html>
​
<!DOCTYPE html>
<html>
​
<body>
  
<script>
class Car {
  constructor(name) {
    this.brand = name;
  }
}
​
const mycar = new Car("Ford");
​
document.write(mycar.brand);
</script>
​
</body>
</html>
​
new Card(cardContainer, {
    id: 'instructions',
    icon: 'fa-solid fa-medal',
    title: 'Contests',
    description: 'Where the magic happens! Manage contests and their teams.',
}).click(async () => {
    const modal = await new Modal(null, { id: 'instructions' }).loadContent('index-instructions');
    modal.addButton({
        text: 'OK, got it!',
        close: true,
    })
});
JavaScript
<script>
document.addEventListener('scroll', function(e) {
var header = document.getElementById('sticky-header');
var scrollTop = window.scrollY || document.documentElement.scrollTop;

if (scrollTop > lastScrollTop) {
// Scroll Down
header.style.transform = 'translateY(-100%)';
} else {
// Scroll Up
header.style.transform = 'translateY(0)';
}
lastScrollTop = scrollTop <= 0 ? 0 : scrollTop; // For Mobile or negative scrolling
}, false);

var lastScrollTop = 0;
</script>

CSS
#sticky-header {
transition: transform 0.3s ease-in-out;
}
<script src="https://cdn.jsdelivr.net/gh/studio-freight/lenis@0.2.28/bundled/lenis.js"></script>

<script>
const lenis = new Lenis({
  duration: 1.2,
  easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // https://www.desmos.com/calculator/brs54l4xou
  direction: 'vertical', // vertical, horizontal
  gestureDirection: 'vertical', // vertical, horizontal, both
  smooth: true,
  mouseMultiplier: 1,
  smoothTouch: false,
  touchMultiplier: 2,
  infinite: false,
})

//get scroll value
lenis.on('scroll', ({ scroll, limit, velocity, direction, progress }) => {
  console.log({ scroll, limit, velocity, direction, progress })
})

function raf(time) {
  lenis.raf(time)
  requestAnimationFrame(raf)
}

requestAnimationFrame(raf)
</script>
import _ from 'lodash';
import dayjs from 'dayjs';
import { getEcgDataForRange } from './../../Store/dbECGData';

export const logError = (msg, error) => {
  console.error(msg, error);
};

const getSizeData = (stop, start, bytesPerSecond) => {
  const diffTime = +stop - +start;
  const sizeData = diffTime * bytesPerSecond;
  return sizeData;
};

const addDataRanges = ({
  summaries, start: startEvent, stop: stopEvent, samplingFrequency, channels,
}) => {
  const bytesPerSample = 2;
  const samplePerMsSecond = channels.length * bytesPerSample * samplingFrequency / 1000;
  const lenHourlySummaries = summaries.length;
  for (let i = 0; i < lenHourlySummaries; i += 1) {
    const hourlySummary = summaries[i];
    const { start: startHs, stop: stopHs } = hourlySummary;
    const _startHs = dayjs(startHs).valueOf();
    const _stopHs = dayjs(stopHs).valueOf();
    const sizeData = getSizeData(
      Math.min(_stopHs, stopEvent),
      Math.max(_startHs, startEvent),
      samplePerMsSecond,
    );
    const readPosition = i === 0
      ? getSizeData(startEvent, Math.min(_startHs, startEvent), samplePerMsSecond)
      : 0;
    hourlySummary.dataRange = {
      start: Math.ceil(readPosition),
      stop: Math.floor(readPosition + sizeData),
    };
  }
};

export const filterHourlySummaryRanges = ({
  summaries, start, stop, samplingFrequency, channels, willAddDataRanges,
}) => {
  const ranges = summaries.data.filter(summary => _.every([
    +summary.stop > +start,
    +summary.start < +stop,
  ]));
  if (willAddDataRanges) {
    addDataRanges({
      summaries: ranges,
      start,
      stop,
      samplingFrequency,
      channels,
    });
  }
  return ranges;
};

export const convertBinaryToECGData = (totalChannel, binaryData) => {
  if (!binaryData) {
    return undefined;
  }
  const ecgData = new Array(totalChannel).fill(null).map(() => []);
  const binaryDataLength = binaryData.length;
  for (let i = 0; i < binaryDataLength; i += totalChannel) {
    for (let j = 0; j < totalChannel; j += 1) {
      ecgData[j].push(binaryData[i + j]);
    }
  }
  return ecgData;
};

export const getSineWaveDataArray = (diffMilliseconds, samplingFrequency, adcGain) => {
  const halfSamplingFrequency = samplingFrequency / 2;
  const sineWaveCount = diffMilliseconds / 1000;

  const sineWaveDataArray = _.map(_.range(samplingFrequency), (x, i) => {
    const sample = Math.sin(x * ((2 * Math.PI) / samplingFrequency)) * (0.75 * (adcGain / 2));
    return i <= halfSamplingFrequency ? Math.round(sample - (1.25 * adcGain)) : Math.round(sample + (1.25 * adcGain));
  });
  let resultSineWave = [];
  const wholeSineWaveCount = Math.floor(sineWaveCount);
  const fractionalSineWaveCount = sineWaveCount - wholeSineWaveCount;
  for (let k = 0; k < wholeSineWaveCount; k += 1) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray);
  }
  if (fractionalSineWaveCount) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray.slice(0, Math.round(fractionalSineWaveCount * sineWaveDataArray.length)));
  }
  return resultSineWave;
};

export const combineRanges = (ranges, start, stop) => {
  const adjustedRanges = ranges
    .map((subarray) => {
      let [subStart, subStop] = subarray;
      subStart = Math.max(subStart, start);
      subStop = Math.min(subStop, stop);
      return subStart <= subStop ? [subStart, subStop] : null;
    })
    .filter(subarray => subarray !== null);

  if (start <= stop) {
    adjustedRanges.unshift([start, start]);
    adjustedRanges.push([stop, stop]);
  }

  return adjustedRanges;
};

export const addSineWaveToEcgData = (selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength = 75000) => {
  if (selectedEcgData[0].length === dataLength) {
    return selectedEcgData;
  }
  const { samplingFrequency, gain } = ecgDataMap;
  const ecgData = selectedEcgData.map(() => []);
  const rangesEpochs = ranges.map(x => [dayjs(x.start).valueOf(), dayjs(x.stop).valueOf()]);
  const startEpoch = dayjs(startMoment).valueOf();
  const stopEpoch = dayjs(stopMoment).valueOf();
  const rangesData = combineRanges(rangesEpochs, startEpoch, stopEpoch);
  let dataIndex = 0;
  rangesData.forEach((range, i) => {
    if (i > 0) {
      const currStart = range[0];
      const currStop = range[1];
      const prevStop = rangesData[i - 1][1];
      if (currStart - prevStop > 0) {
        const sineWave = getSineWaveDataArray(currStart - prevStop, samplingFrequency, gain);
        ecgData.forEach((data) => {
          data.push(...sineWave);
        });
      }
      if (currStop - currStart > 0) {
        const diffMs = (currStop - currStart) / 1000;
        const dataLength = Math.round(diffMs * samplingFrequency);
        ecgData.forEach((data, j) => {
          data.push(...selectedEcgData[j].slice(dataIndex, dataIndex + dataLength));
        });
        dataIndex += dataLength;
      }
    }
  });
  return ecgData;
};

export const downloadSelectedEcgDataFromDB = async (ranges, ecgData) => {
  try {
    if (!ranges.length) {
      return [];
    }

    // chạy vòng for thay cho (map, filter)
    const blobParts = ranges.map((range) => {
      if (ecgData) {
        for (const ecg of ecgData) {
          if (range.dataRange.start >= 0 && range.dataRange.stop <= ecg.byteLength) {
            return ecg.slice(range.dataRange.start, range.dataRange.stop + 1);
          }
        }
      }
      return null;
    });
    const filteredBlobParts = blobParts.filter(data => data !== null);
    if (filteredBlobParts.length === 0) {
      throw new Error('No valid data ranges found');
    }

    let concatenated = await new Blob(filteredBlobParts).arrayBuffer();
    const { byteLength } = concatenated;
    if (byteLength % 2 !== 0) {
      concatenated = concatenated.slice(0, byteLength - 1);
    }

    return new Int16Array(concatenated);
  } catch (error) {
    logError('Failed to download selected ECG data from IndexedDB: ', error);
    return [];
  }
};

/**
 * Downloads ECG data for a specific beat and offset range.
 *
 * @param {Object} options - The options for downloading the data.
 * @param {Object} options.ecgDataMap - The ECG data map containing channels and sampling frequency.
 * @param {Object} options.beat - The beat object containing the id.
 * @param {number} options.offset - The offset range in milliseconds.
 * @param {number} [options.dataLength=75000] - The length of the data to be downloaded.
 * @return {Promise<Array>} - A promise that resolves to the downloaded ECG data with sine wave added.
 * @throws {Error} - If there is an error in processing the downloaded ECG data.
 */
export const handleDownloadData = async ({ ecgDataMap, beat, offset, dataLength }) => {
  try {
    const { channels, samplingFrequency } = ecgDataMap;
    let epoch = beat?.id;
    const dividedNumber = 1000 / samplingFrequency;
    epoch = Math.floor(epoch / dividedNumber) * dividedNumber;

    const startMoment = dayjs(epoch - offset).valueOf();
    const stopMoment = dayjs(epoch + offset).valueOf();
    const _ecgDataMap = _.cloneDeep(ecgDataMap);

    // Lấy ra các khoảng thời gian 
    const ranges = filterHourlySummaryRanges({
      summaries: _ecgDataMap,
      start: startMoment,
      stop: stopMoment,
      samplingFrequency,
      channels,
      willAddDataRanges: true
    });

    // Lấy dữ liệu ECG từ IndexedDB
    const ecgData = await getEcgDataForRange(startMoment, stopMoment);
    // Tải về dữ liệu ECG đã chọn từ DB
    if (ecgData) {
      const downloadedSelectedEcgData = await downloadSelectedEcgDataFromDB(ranges, ecgData);
      if (downloadedSelectedEcgData?.length) {
        const selectedEcgData = convertBinaryToECGData(channels.length, downloadedSelectedEcgData);
        const sineWaveData = addSineWaveToEcgData(selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength);
        return sineWaveData;
      }
    }
    throw new Error('Error in processing downloaded ECG data');
  } catch (error) {
    logError('Failed to handle download data: ', error);
    return [];
  }
};
import _ from 'lodash';

function splitBorderBeats(eventStart, eventStop, holterBeat, samplingFrequency) {
  const { start: startHb, beatPositions = [], hesBeatStatus = [] } = holterBeat;

  let beatStartIndex = 0;
  if (eventStart) {
    const beatStartPosition = (+eventStart - +startHb) * samplingFrequency / 1000;
    beatStartIndex = _.findIndex(beatPositions, beat => beat >= beatStartPosition);
  }

  let beatStopIndex = beatPositions.length;
  if (eventStop) {
    const beatStopPosition = (+eventStop - +startHb) * samplingFrequency / 1000;
    beatStopIndex = _.findLastIndex(beatPositions, beat => beat <= beatStopPosition) + 1;
  }

  // Kiểm tra lại các chỉ số trước khi cắt dữ liệu
  if (beatStartIndex < 0) beatStartIndex = 0;
  if (beatStopIndex > beatPositions.length) beatStopIndex = beatPositions.length;

  // Trả về các chỉ số để xử lý mà không thay đổi dữ liệu gốc
  return {
    beatPositions: _.slice(beatPositions, beatStartIndex, beatStopIndex),
    hesBeatStatus: _.slice(hesBeatStatus, beatStartIndex, beatStopIndex),
  };
}

function convertBeats(eventStart, holterBeat, samplingFrequency) {
  const { start: startHb, beatPositions = [] } = holterBeat;
  const beatOffset = (+eventStart - +startHb) * samplingFrequency / 1000;
  const roundBeatOffset = _.round(beatOffset);

  return _.map(beatPositions, x => x - roundBeatOffset);
}

/**
 * Main function to process beats data.
 * @param {Object} filter - The filter criteria.
 * @returns {Object} - The result object containing beat positions and statuses.
 */
export function processBeatsData(filter) {
  const { start, stop, samplingFrequency = 1000, data = [] } = filter;
  const eventStart = new Date(start);
  const eventStop = new Date(stop);

  // Tạo bản sao dữ liệu để tránh ảnh hưởng đến dữ liệu gốc
  const clonedData = _.cloneDeep(data);

  // Sort by start
  const sortedBeats = _.map(clonedData, x => {
    x.start = new Date(x.start);
    x.stop = new Date(x.stop);
    return x;
  });

  let beatPositions = [];
  let hesBeatStatus = [];

  if (sortedBeats.length) {
    if (sortedBeats.length === 1) {
      const result = splitBorderBeats(eventStart, eventStop, _.first(sortedBeats), samplingFrequency);
      beatPositions = convertBeats(eventStart, { ..._.first(sortedBeats), beatPositions: result.beatPositions }, samplingFrequency);
      hesBeatStatus = result.hesBeatStatus;
    } else {
      const firstResult = splitBorderBeats(eventStart, null, _.first(sortedBeats), samplingFrequency);
      const lastResult = splitBorderBeats(null, eventStop, _.last(sortedBeats), samplingFrequency);

      beatPositions = convertBeats(eventStart, { ..._.first(sortedBeats), beatPositions: firstResult.beatPositions }, samplingFrequency)
        .concat(convertBeats(eventStart, { ..._.last(sortedBeats), beatPositions: lastResult.beatPositions }, samplingFrequency));

      hesBeatStatus = firstResult.hesBeatStatus.concat(lastResult.hesBeatStatus);
    }
  } else {
    console.log('could not found beats', { start, stop });
  }

  return {
    isSuccess: true,
    beatPositions,
    hesBeatStatus,
  };
}
import dayjs from 'dayjs';
import {
  openDatabase, getData, upsertData, addMultipleData, clearObjectStore,
} from './utils';
import { checkHasCachesECGHourly, getCachesECGHourly, setCachesECGHourly } from "./caches";

// Constants for ECG Info Database
const ECG_DB_NAME = 'DB_ECG_WORKER_INFO';
const ECG_OBJECT_STORE_NAME = 'ECG_WORKER';
const ECG_OBJECT_STORE_KEY = 'key';
const ECG_DB_VERSION = 1;
let ecgInfoDB = null;

// Common function to open databases
const openDBEcgData = async () => {
  try {
    ecgInfoDB = await openDatabase(ECG_DB_NAME, ECG_DB_VERSION, ECG_OBJECT_STORE_NAME, ECG_OBJECT_STORE_KEY);
  } catch (error) {
    console.error('Failed to open databases', error);
  }
};

const clearDBEcgData = async () => {
  if (ecgInfoDB) {
    await clearObjectStore(ecgInfoDB, ECG_OBJECT_STORE_NAME);
  }
};

const addEcgData = (key, value) => upsertData(ecgInfoDB, ECG_OBJECT_STORE_NAME, key, value);
const addMultipleECGData = async dataArray => await addMultipleData(ecgInfoDB, ECG_OBJECT_STORE_NAME, dataArray);
const getEcgData = key => getData(ecgInfoDB, ECG_OBJECT_STORE_NAME, key);

// Helper functions to calculate start and end of an hour
const getStartOfHour = epochTime => dayjs(epochTime).startOf('hour').valueOf();
const getEndOfHour = epochTime => dayjs(epochTime).endOf('hour').valueOf();

// Helper function to get data for a specific hour
const getDataForHour = async (db, storeName, epochTime) => {
  const startOfHour = getStartOfHour(epochTime);
  const endOfHour = getEndOfHour(epochTime);

  const keyCache = `${startOfHour}-${endOfHour}`;
  if (checkHasCachesECGHourly(keyCache)) {
    return getCachesECGHourly(keyCache);
  }

  const dataECGHourly = await getData(db, storeName, startOfHour);
  setCachesECGHourly(keyCache, dataECGHourly);
  return dataECGHourly;
};

const fetchECGHourData = async (epoch, needPrevious, needNext) => {
  let dataInRange = [];
  dataInRange = dataInRange.concat(await getDataForHour(ecgInfoDB, ECG_OBJECT_STORE_NAME, epoch));
  if (needPrevious) dataInRange = dataInRange.concat(await getDataForHour(ecgInfoDB, ECG_OBJECT_STORE_NAME, epoch - 3600 * 1000));
  if (needNext) dataInRange = dataInRange.concat(await getDataForHour(ecgInfoDB, ECG_OBJECT_STORE_NAME, epoch + 3600 * 1000));
  return dataInRange;
};

// Main function to get data within a range for ECG data
const getEcgDataForRange = async (startEpoch, endEpoch) => {
  const dataInRangeResult = [];
  const needPreviousHour = (startEpoch - getStartOfHour(startEpoch)) < 0;
  const needNextHour = (getEndOfHour(endEpoch) - endEpoch) < 0;
  const dataInRange = await fetchECGHourData(startEpoch, needPreviousHour, needNextHour);

  for (const data of dataInRange) {
    dataInRangeResult.push(data.value);
  }
  return dataInRangeResult;
};

export {
  openDBEcgData,
  getEcgData,
  addEcgData,
  getEcgDataForRange,
  addMultipleECGData,
};
/* eslint-disable consistent-return */
/* eslint-disable prefer-promise-reject-errors */
import dayjs from 'dayjs';
import {
  openDatabase, getData, upsertData, addMultipleData, clearObjectStore,
} from './utils';
import { checkHasCachesBeatHourly, getCachesBeatHourly, setCachesBeatHourly } from "./caches";

// Constants for Beat Info Database
const BEAT_DB_NAME = 'DB_BEAT_WORKER_INFO';
const BEAT_OBJECT_STORE_NAME = 'BEAT_WORKER';
const BEAT_OBJECT_STORE_KEY = 'key';
const BEAT_DB_VERSION = 1;
let beatInfoDB = null;

// Common function to open databases
const openDBBeatData = async () => {
  try {
    beatInfoDB = await openDatabase(BEAT_DB_NAME, BEAT_DB_VERSION, BEAT_OBJECT_STORE_NAME, BEAT_OBJECT_STORE_KEY);
    // await clearDBBeatData();
  } catch (error) {
    console.error('Failed to open databases', error);
  }
};

const clearDBBeatData = async () => {
  if (beatInfoDB) {
    await clearObjectStore(beatInfoDB, BEAT_OBJECT_STORE_NAME);
  }
};

const addBeatData = (key, value) => upsertData(beatInfoDB, BEAT_OBJECT_STORE_NAME, key, value);
const getBeatData = key => getData(beatInfoDB, BEAT_OBJECT_STORE_NAME, key);
const addMultipleBeatData = async dataArray => await addMultipleData(beatInfoDB, BEAT_OBJECT_STORE_NAME, dataArray);

// Helper functions to calculate start and end of an hour
const getStartOfHour = epochTime => dayjs(epochTime).startOf('hour').valueOf();
const getEndOfHour = epochTime => dayjs(epochTime).endOf('hour').valueOf();

// Helper function to get data for a specific hour
const getDataForHour = async (db, storeName, epochTime) => {
  const startOfHour = getStartOfHour(epochTime);
  const endOfHour = getEndOfHour(epochTime);

  const keyCache = `${startOfHour}-${endOfHour}`;
  if (checkHasCachesBeatHourly(keyCache)) {
    return getCachesBeatHourly(keyCache);
  }

  const dataECGHourly = await getData(db, storeName, startOfHour);
  setCachesBeatHourly(keyCache, dataECGHourly);
  return dataECGHourly;
};

// Helper function to merge two data objects
const mergeBeatData = (data1, data2) => ({
  start: data1.start,
  stop: data2.stop,
  beatPositions: data1.beatPositions.concat(data2.beatPositions),
  beatEpochs: data1.beatEpochs.concat(data2.beatEpochs),
  hesBeatStatus: data1.hesBeatStatus.concat(data2.hesBeatStatus)
});

// Fetch and merge ECG hour data
const fetchECGHourData = async (epoch, needPrevious, needNext) => {
  const currentData = await getDataForHour(beatInfoDB, BEAT_OBJECT_STORE_NAME, epoch);
  let mergedData = currentData.value;

  if (needPrevious) {
    const previousData = await getDataForHour(beatInfoDB, BEAT_OBJECT_STORE_NAME, epoch - 3600 * 1000);
    mergedData = mergeBeatData(previousData.value, mergedData);
  }

  if (needNext) {
    const nextData = await getDataForHour(beatInfoDB, BEAT_OBJECT_STORE_NAME, epoch + 3600 * 1000);
    mergedData = mergeBeatData(mergedData, nextData.value);
  }

  return mergedData;
};

// Main function to get data within a range for ECG data
const getBeatDataForRange = async (startEpoch, endEpoch, offset = 2000) => {
  const startOfHour = getStartOfHour(startEpoch);
  const endOfHour = getEndOfHour(endEpoch);

  const needPreviousHour = (startEpoch - startOfHour) < offset;
  const needNextHour = (endOfHour - endEpoch) < offset;

  const mergedData = await fetchECGHourData(startEpoch, needPreviousHour, needNextHour);

  // Nếu startEpoch gần cuối giờ, cần lấy thêm dữ liệu giờ tiếp theo
  if (startEpoch >= getEndOfHour(startEpoch) - 2000) {
    const nextData = await fetchECGHourData(endEpoch, false, true);
    return [mergeBeatData(mergedData, nextData)];
  }

  return [mergedData];
};

export {
  openDBBeatData,
  getBeatData,
  addBeatData,
  getBeatDataForRange,
  addMultipleBeatData,
  mergeBeatData,
  fetchECGHourData
};
The Canvas API can be used to create graphics in games using JavaScript and the HTML canvas element.

You will need to use the getContext method which will provide the context for where the graphics will be rendered.

Example Code
canvas.getContext("2d");

The canvas element has a width property which is a positive number that represents the width of the canvas.

Example Code
canvas.width

The innerWidth property is a number that represents the interior width of the browser window.

The innerHeight property is a number that represents the interior height of the browser window.

Here is the syntax for using the destructuring assignment in the parameter list of the arrow function:

Example Code
btn.addEventListener('click', ({ target }) => {
  console.log(target);
});

When working with objects where the property name and value are the same, you can use the shorthand property name syntax. This syntax allows you to omit the property value if it is the same as the property name.

Example Code
// using shorthand property name syntax
obj = {
  a, b, c
}
The following code is the same as:

Example Code
obj = {
  a: a,
  b: b,
  c: c
}
/* eslint-disable consistent-return */
/* eslint-disable prefer-promise-reject-errors */
import dayjs from 'dayjs';
import {
  openDatabase, getData, getDataInRange, upsertData, addMultipleData, clearObjectStore,
} from './utils';

// Constants for Beat Info Database
const BEAT_DB_NAME = 'DB_BEAT_WORKER_INFO';
const BEAT_OBJECT_STORE_NAME = 'BEAT_WORKER';
const BEAT_OBJECT_STORE_KEY = 'key';
const BEAT_DB_VERSION = 1;
let beatInfoDB = null;

// Common function to open databases
const openDBBeatData = async () => {
  try {
    beatInfoDB = await openDatabase(BEAT_DB_NAME, BEAT_DB_VERSION, BEAT_OBJECT_STORE_NAME, BEAT_OBJECT_STORE_KEY);
    await clearDBBeatData();
  } catch (error) {
    console.error('Failed to open databases', error);
  }
};

const clearDBBeatData = async () => {
    if (ecgInfoDB) {
      await clearObjectStore(beatInfoDB, BEAT_OBJECT_STORE_NAME);
    }
  };
  

const addBeatData = (key, value) => upsertData(beatInfoDB, BEAT_OBJECT_STORE_NAME, key, value);
const getBeatData = key => getData(beatInfoDB, BEAT_OBJECT_STORE_NAME, key);
const addMultipleBeatData = async dataArray => await addMultipleData(beatInfoDB, BEAT_OBJECT_STORE_NAME, dataArray);

// Helper functions to calculate start and end of an hour
const getStartOfHour = epochTime => dayjs(epochTime).startOf('hour').valueOf();
const getEndOfHour = epochTime => dayjs(epochTime).endOf('hour').valueOf();

// Main function to get data within a range for Beat data
const getBeatDataForRange = async (startEpoch, endEpoch) => {
  const dataInRangeResult = [];
  const startHour = getStartOfHour(startEpoch);
  const endHour = getEndOfHour(endEpoch);

  if (getStartOfHour(startEpoch) === getStartOfHour(endEpoch)) {
    // Both timestamps are within the same hour
    const dataInRange = await getDataInRange(beatInfoDB, BEAT_OBJECT_STORE_NAME, startHour, endHour);
    if (dataInRange && dataInRange.length > 0) {
      const data = dataInRange[0].value;
      console.log("data",data);
      dataInRangeResult.push(data);
    }
  } else {
    // Timestamps span across different hours
    const startHourData = await getDataInRange(beatInfoDB, BEAT_OBJECT_STORE_NAME, getStartOfHour(startEpoch), getEndOfHour(startEpoch));
    const endHourData = await getDataInRange(beatInfoDB, BEAT_OBJECT_STORE_NAME, getStartOfHour(endEpoch), getEndOfHour(endEpoch));

    if (startHourData && startHourData.length > 0) {
      const startData = startHourData[0].value;
      dataInRangeResult.push(startData);
    }
    if (endHourData && endHourData.length > 0) {
      const endData = endHourData[0].value;
      dataInRangeResult.push(endData);
    }
  }
  console.log("dataInRangeResult",dataInRangeResult);
  return dataInRangeResult;
};

export {
  openDBBeatData,
  getBeatData,
  addBeatData,
  getBeatDataForRange,
  addMultipleBeatData,
};
const user = {
  
  "intro":  {
    "name" : "Saud",
    "age" : 20,
    "qualification": "BS Computer"
  },
  
  "address": {
    "country" : "Pakistan",
    "province" : "KP",
    "city" : "Mardan"
  },
  
  userIntro(){
    return `Name: ${this.intro.name}, age: ${this.intro.age}, qualification: ${this.intro.qualification}`;
  },

  userAddress(){
    return `country: ${this.address.country}, province: ${this.address.province}, city: ${this.address.city}`
  }
}

// let name = user.intro.name;
// console.log(name);

let userData = user.userIntro();
console.log(userData);

let addressData = user.userAddress()
console.log(addressData);

  content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],

   
   
   
   
   da babelshi es --> 
     
  plugins: ["nativewind/babel"],
npm install --save-dev tailwindcss@latest
 npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
npx create-expo-app@latest [your-app-name] --template blank@latest  
You are already familiar with an HTML class, but JavaScript also has a class. In JavaScript, a class is like a blueprint for creating objects. It allows you to define a set of properties and methods, and instantiate (or create) new objects with those properties and methods.

The class keyword is used to declare a class. Here is an example of declaring a Computer class:

Example Code
class Computer {};

Classes have a special constructor method, which is called when a new instance of the class is created. The constructor method is a great place to initialize properties of the class. Here is an example of a class with a constructor method:

Example Code
class Computer {
  constructor() {
  }
}

The this keyword in JavaScript is used to refer to the current object. Depending on where this is used, what it references changes. In the case of a class, it refers to the instance of the object being constructed. You can use the this keyword to set the properties of the object being instantiated. Here is an example:

Example Code
class Computer {
  constructor() {
    this.ram = 16;
  }
}

Here is an example of instantiating the Computer class from earlier examples:

Example Code
const myComputer = new Computer();

var link = React.DOM.a({
                    href: this.makeHref('login')
                },
                'log in'
            );// or React.createElement or
//var link = <a href={this.makeHref('login')}>
//   'log in'</a>;
<div>{'Please '+ link + ' with your email...'}</div>
Functions are ideal for reusable logic. When a function itself needs to reuse logic, you can declare a nested function to handle that logic. Here is an example of a nested function:

Example Code
const outer = () => {
  const inner = () => {

  };
};

Object properties consist of key/value pairs. You can use shorthand property names when declaring an object literal. When using the shorthand property name syntax, the name of the variable becomes the property key and its value the property value.

The following example declares a user object with the properties userId, firstName, and loggedIn.

Example Code
const userId = 1;
const firstName = "John";
const loggedIn = true;

const user = {
  userId,
  firstName,
  loggedIn,
};

console.log(user); // { userId: 1, firstName: 'John', loggedIn: true }

The concept of returning a function within a function is called currying. This approach allows you to create a variable that holds a function to be called later, but with a reference to the parameters of the outer function call.

For example:

Example Code
const innerOne = elemValue(1);
const final = innerOne("A");
innerOne would be your inner function, with num set to 1, and final would have the value of the cell with the id of A1. This is possible because functions have access to all variables declared at their creation. This is called closure.

In your elemValue function, you explicitly declared a function called inner and returned it. However, because you are using arrow syntax, you can implicitly return a function. For example:

Example Code
const curry = soup => veggies => {};
curry is a function which takes a soup parameter and returns a function which takes a veggies parameter.

You can pass a function reference as a callback parameter. A function reference is a function name without the parentheses. For example:

Example Code
const myFunc = (val) => `value: ${val}`;
const array = [1, 2, 3];
const newArray = array.map(myFunc);
The .map() method here will call the myFunc function, passing the same arguments that a .map() callback takes. The first argument is the value of the array at the current iteration, so newArray would be [value: 1, value: 2, value: 3].

Arrays have a .some() method. Like the .filter() method, .some() accepts a callback function which should take an element of the array as the argument. The .some() method will return true if the callback function returns true for at least one element in the array.

Here is an example of a .some() method call to check if any element in the array is an uppercase letter.

Example Code
const arr = ["A", "b", "C"];
arr.some(letter => letter === letter.toUpperCase());

Arrays have an .every() method. Like the .some() method, .every() accepts a callback function which should take an element of the array as the argument. The .every() method will return true if the callback function returns true for all elements in the array.

Here is an example of a .every() method call to check if all elements in the array are uppercase letters.

Example Code
const arr = ["A", "b", "C"];
arr.every(letter => letter === letter.toUpperCase());
The .split() method takes a string and splits it into an array of strings. You can pass it a string of characters or a RegEx to use as a separator. For example, string.split(",") would split the string at each comma and return an array of strings.

The .map() method takes a callback function as its first argument. This callback function takes a few arguments, but the first one is the current element being processed. Here is an example:

Example Code
array.map(el => {

})

Much like the .map() method, the .filter() method takes a callback function. The callback function takes the current element as its first argument.

Example Code
array.filter(el => {

})

Array methods can often be chained together to perform multiple operations at once. As an example:

Example Code
array.map().filter();
The .map() method is called on the array, and then the .filter() method is called on the result of the .map() method. This is called method chaining.

The .reduce() method takes an array and applies a callback function to condense the array into a single value.

Like the other methods, .reduce() takes a callback. This callback, however, takes at least two parameters. The first is the accumulator, and the second is the current element in the array. The return value for the callback becomes the value of the accumulator on the next iteration.

Example Code
array.reduce((acc, el) => {

});

The .reduce() method takes a second argument that is used as the initial value of the accumulator. Without a second argument, the .reduce() method uses the first element of the array as the accumulator, which can lead to unexpected results.

To be safe, it's best to set an initial value. Here is an example of setting the initial value to an empty string:

Example Code
array.reduce((acc, el) => acc + el.toLowerCase(), "");

By default, the .sort() method converts the elements of an array into strings, then sorts them alphabetically. This works well for strings, but not so well for numbers. For example, 10 comes before 2 when sorted as strings, but 2 comes before 10 when sorted as numbers.

To fix this, you can pass in a callback function to the .sort() method. This function takes two arguments, which represent the two elements being compared. The function should return a value less than 0 if the first element should come before the second element, a value greater than 0 if the first element should come after the second element, and 0 if the two elements should remain in their current positions.

You previously learned about the global Math object. Math has a .min() method to get the smallest number from a series of numbers, and the .max() method to get the largest number. Here's an example that gets the smallest number from an array:

Example Code
const numbersArr = [2, 3, 1];

console.log(Math.min(...numbersArr));
// Expected output: 1

To calculate a root exponent, such as x−−√n
, you can use an inverted exponent x1/n
. JavaScript has a built-in Math.pow() function that can be used to calculate exponents.

Here is the basic syntax for the Math.pow() function:

Example Code
Math.pow(base, exponent);
Here is an example of how to calculate the square root of 4:

Example Code
const base = 4;
const exponent = 0.5;
// returns 2
Math.pow(base, exponent);
window.pixelmatch = (() => {
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __commonJS = (cb, mod) => function __require() {
    return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
  };

  // entry.js
  var require_entry = __commonJS({
    "entry.js"(exports, module) {
      module.exports = pixelmatch;
      var defaultOptions = {
        threshold: 0.1,
        // matching threshold (0 to 1); smaller is more sensitive
        includeAA: false,
        // whether to skip anti-aliasing detection
        alpha: 0.1,
        // opacity of original image in diff output
        aaColor: [255, 255, 0],
        // color of anti-aliased pixels in diff output
        diffColor: [255, 0, 0],
        // color of different pixels in diff output
        diffColorAlt: null,
        // whether to detect dark on light differences between img1 and img2 and set an alternative color to differentiate between the two
        diffMask: false
        // draw the diff over a transparent background (a mask)
      };
      function pixelmatch(img1, img2, output, width, height, options) {
        if (!isPixelData(img1) || !isPixelData(img2) || output && !isPixelData(output))
          throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");
        if (img1.length !== img2.length || output && output.length !== img1.length)
          throw new Error("Image sizes do not match.");
        if (img1.length !== width * height * 4) throw new Error("Image data size does not match width/height.");
        options = Object.assign({}, defaultOptions, options);
        const len = width * height;
        const a32 = new Uint32Array(img1.buffer, img1.byteOffset, len);
        const b32 = new Uint32Array(img2.buffer, img2.byteOffset, len);
        let identical = true;
        for (let i = 0; i < len; i++) {
          if (a32[i] !== b32[i]) {
            identical = false;
            break;
          }
        }
        if (identical) {
          if (output && !options.diffMask) {
            for (let i = 0; i < len; i++) drawGrayPixel(img1, 4 * i, options.alpha, output);
          }
          return 0;
        }
        const maxDelta = 35215 * options.threshold * options.threshold;
        let diff = 0;
        for (let y = 0; y < height; y++) {
          for (let x = 0; x < width; x++) {
            const pos = (y * width + x) * 4;
            const delta = colorDelta(img1, img2, pos, pos);
            if (Math.abs(delta) > maxDelta) {
              if (!options.includeAA && (antialiased(img1, x, y, width, height, img2) || antialiased(img2, x, y, width, height, img1))) {
                if (output && !options.diffMask) drawPixel(output, pos, ...options.aaColor);
              } else {
                if (output) {
                  drawPixel(output, pos, ...delta < 0 && options.diffColorAlt || options.diffColor);
                }
                diff++;
              }
            } else if (output) {
              if (!options.diffMask) drawGrayPixel(img1, pos, options.alpha, output);
            }
          }
        }
        return diff;
      }
      function isPixelData(arr) {
        return ArrayBuffer.isView(arr) && arr.constructor.BYTES_PER_ELEMENT === 1;
      }
      function antialiased(img, x1, y1, width, height, img2) {
        const x0 = Math.max(x1 - 1, 0);
        const y0 = Math.max(y1 - 1, 0);
        const x2 = Math.min(x1 + 1, width - 1);
        const y2 = Math.min(y1 + 1, height - 1);
        const pos = (y1 * width + x1) * 4;
        let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
        let min = 0;
        let max = 0;
        let minX, minY, maxX, maxY;
        for (let x = x0; x <= x2; x++) {
          for (let y = y0; y <= y2; y++) {
            if (x === x1 && y === y1) continue;
            const delta = colorDelta(img, img, pos, (y * width + x) * 4, true);
            if (delta === 0) {
              zeroes++;
              if (zeroes > 2) return false;
            } else if (delta < min) {
              min = delta;
              minX = x;
              minY = y;
            } else if (delta > max) {
              max = delta;
              maxX = x;
              maxY = y;
            }
          }
        }
        if (min === 0 || max === 0) return false;
        return hasManySiblings(img, minX, minY, width, height) && hasManySiblings(img2, minX, minY, width, height) || hasManySiblings(img, maxX, maxY, width, height) && hasManySiblings(img2, maxX, maxY, width, height);
      }
      function hasManySiblings(img, x1, y1, width, height) {
        const x0 = Math.max(x1 - 1, 0);
        const y0 = Math.max(y1 - 1, 0);
        const x2 = Math.min(x1 + 1, width - 1);
        const y2 = Math.min(y1 + 1, height - 1);
        const pos = (y1 * width + x1) * 4;
        let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
        for (let x = x0; x <= x2; x++) {
          for (let y = y0; y <= y2; y++) {
            if (x === x1 && y === y1) continue;
            const pos2 = (y * width + x) * 4;
            if (img[pos] === img[pos2] && img[pos + 1] === img[pos2 + 1] && img[pos + 2] === img[pos2 + 2] && img[pos + 3] === img[pos2 + 3]) zeroes++;
            if (zeroes > 2) return true;
          }
        }
        return false;
      }
      function colorDelta(img1, img2, k, m, yOnly) {
        let r1 = img1[k + 0];
        let g1 = img1[k + 1];
        let b1 = img1[k + 2];
        let a1 = img1[k + 3];
        let r2 = img2[m + 0];
        let g2 = img2[m + 1];
        let b2 = img2[m + 2];
        let a2 = img2[m + 3];
        if (a1 === a2 && r1 === r2 && g1 === g2 && b1 === b2) return 0;
        if (a1 < 255) {
          a1 /= 255;
          r1 = blend(r1, a1);
          g1 = blend(g1, a1);
          b1 = blend(b1, a1);
        }
        if (a2 < 255) {
          a2 /= 255;
          r2 = blend(r2, a2);
          g2 = blend(g2, a2);
          b2 = blend(b2, a2);
        }
        const y1 = rgb2y(r1, g1, b1);
        const y2 = rgb2y(r2, g2, b2);
        const y = y1 - y2;
        if (yOnly) return y;
        const i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2);
        const q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);
        const delta = 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
        return y1 > y2 ? -delta : delta;
      }
      function rgb2y(r, g, b) {
        return r * 0.29889531 + g * 0.58662247 + b * 0.11448223;
      }
      function rgb2i(r, g, b) {
        return r * 0.59597799 - g * 0.2741761 - b * 0.32180189;
      }
      function rgb2q(r, g, b) {
        return r * 0.21147017 - g * 0.52261711 + b * 0.31114694;
      }
      function blend(c, a) {
        return 255 + (c - 255) * a;
      }
      function drawPixel(output, pos, r, g, b) {
        output[pos + 0] = r;
        output[pos + 1] = g;
        output[pos + 2] = b;
        output[pos + 3] = 255;
      }
      function drawGrayPixel(img, i, alpha, output) {
        const r = img[i + 0];
        const g = img[i + 1];
        const b = img[i + 2];
        const val = blend(rgb2y(r, g, b), alpha * img[i + 3] / 255);
        drawPixel(output, i, val, val, val);
      }
    }
  });
  return require_entry();
})();
// ==UserScript==
// @name         spys.one proxy parser
// @namespace    iquaridys:hideme-parser-proxy
// @version      0.1
// @description  parse proxy from site page
// @author       iquaridys
// @match        http://spys.one/*/
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';
    GM_registerMenuCommand('Parse', function() {
        var resultText = "";
        var a = document.getElementsByClassName('spy14');
            for(var i=0;i<a.length;i++){
                if(a[i].innerText.includes(':')){
                    resultText += a[i].innerText+"<br>";
                }
            }
        var win = window.open("about:blank", "proxy", "width=500,height=400");
        win.document.write(resultText);
    });
})();
<script type="text/javascript" async>
    ////add Attrs alt to images	
	function addAltAttrs() {
			
    //get the images
    let images = document.querySelectorAll("img"); 
     
    //loop through all images
    for (let i = 0; i < images.length; i++) {
		
       //check if alt missing
       if  ( !images[i].alt || images[i].alt == "" || images[i].alt === "") {
		//add file name to alt
         images[i].alt = images[i].src.match(/.*\/([^/]+)\.([^?]+)/i)[1];
       }
    } 
    // end loop
}
</script>
<!-- in css: -->
<style>
  #p-bar-wrapper {
   	display:none;
   	position: fixed;
   	bottom: 0;
   	right: 0;
   	width: 100vw;
   	height: 8px;
   	background-color:#d1d6d8;
   	z-index: 18;
  }
  #progress-bar {
   	background-color:#295b71;
   	width: 0;
   	height: 8px;
   	transition: .3s;
  }
  #progress-bar span {
   	position: absolute;
   	color: #42616c;
   	top: -18px;
   	font-size: 0.8em;
   	font-weight: 600;
   	margin-right:0;
  }
  #run-bar.right-fix {
   	margin-right: -80px;
  }
  #run-bar.right-fix-2one {
   	margin-right: -77px;
  }
</style>

<!-- in html: -->
<div id="p-bar-wrapper">
    <div id="progress-bar"><span id="run-bar"></span></div>
</div>

<!-- in js: -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js" crossorigin="anonymous"></script>
<script type="text/javascript" async>
    document.addEventListener("DOMContentLoaded", function() {
    	  document.addEventListener("scroll", function() {
    	    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
    	    var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
    		var clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
    		var windowWidth = window.innerWidth;
    		var p_bar =	document.getElementById("progress-bar");
    		var scrolled = (scrollTop / (scrollHeight - clientHeight)) * 100;
     
     	//Check if Tablet or smaller than hide all progress bar
    		if( windowWidth < 767 ){
    			return;
    		}
    		else {
    			jQuery("#p-bar-wrapper").css('display','block').show('slow');
    			p_bar.style.width = scrolled + "%";
    			var scrolled_num = parseInt(scrolled, 10);
    			var span_run = document.getElementById("run-bar");
    			jQuery(span_run).text(scrolled_num + '%').css('right',scrolled + '%');
    				if (scrolled == 100){
    					jQuery(span_run).addClass('right-fix-2one').css('color','#21f1af');
    				}
    				else {
    					jQuery(span_run).removeClass('right-fix-2one').css('color','#42616c');
    					if (scrolled > 15){
    						jQuery(span_run).addClass('right-fix');
    					}
    					else {
    						jQuery(span_run).removeClass('right-fix');
    					}
    				}
    			}
    		});
    	});	
</script>
<!-- in css: -->
<style>
	#up-btn {
      position: fixed;
      bottom: 20px;
      right: 20px;
  	  z-index: 15;
	  cursor: pointer;
      transition: all .3s;
  	}
 	img[src$=".svg"]{
		width:48px
    }
</style>

<!-- in html: -->
<div class="btn-hide" data-id="" data-element_type="widget" id="up-btn" data-settings="" alt="scroll to top">
	<img width="40" height="55" src="https://aspx.co.il/wp-content/uploads/2023/04/arrowup.svg" class="" alt="arrowup" /> 
</div>

<!-- in js: -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js" crossorigin="anonymous"></script>
<script type="text/javascript" async>
	jQuery(document).ready(function($){
  
	//Check to see if the window is top if more then 500px from top display button
	$(window).scroll(function(){
		if ($(this).scrollTop() > 500) {
			$('#up-btn').fadeIn(300,'linear').removeClass('btn-hide');
		} else {
			$('#up-btn').fadeOut(200).hide('slow').addClass('btn-hide');
		}
	});

	//Click event to scroll to top
	$('#up-btn').click(function(){
		$('html, body').animate({scrollTop : 0},800);
		$(this).addClass('btn-hide');
			return false;
	});
</script>

/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
import axios from 'axios';
import { expose } from 'threads/worker';
import _, { chunk } from 'lodash';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
import TTLCache from '@isaacs/ttlcache';
import studySummariesJson from '../../dummyData/studySummaries.json';

dayjs.extend(isBetween);

const fileUrls = [
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-08-08-14%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-09-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-10-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-11-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-12-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-13-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-14-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-15-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-16-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-17-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-18-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-19-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-20-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-21-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-22-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-23-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-00-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-01-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-02-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-03-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-04-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-05-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-06-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-07-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-08-00-00%2B28-final.beat',
];

const CACHE_ECG = new TTLCache({ ttl: 180000 }); // Cache ECG files for 3 minutes
const CACHE_BEAT = new TTLCache({ ttl: 180000 }); // Cache BEAT files for 3 minutes
const config = {};

function extractFilename(url) {
  // Sử dụng URL API để lấy pathname từ URL
  const urlObj = new URL(url);
  const { pathname } = urlObj;

  // Sử dụng Regular Expression để lấy tên tệp từ pathname
  const regex = /[^/]+$/; // Biểu thức chính quy để tìm phần cuối cùng sau dấu "/"
  const match = pathname.match(regex);

  if (match) {
    return match[0];
  }
  return null;
}

const downloadFile = async (url, cache) => {
  const filename = extractFilename(url);
  if (cache.has(filename)) {
    return cache.get(filename);
  }
  try {
    const response = await axios({
      method: 'GET',
      url,
      responseType: 'arraybuffer',
    });

    cache.set(filename, response.data);
    return response.data;
  } catch (error) {
    console.error('Failed to download file: ', error);
    return null;
  }
};

const getECGDataForHour = async (hourIndex, ecgDataMap) => {
  if (hourIndex < 0 || hourIndex >= ecgDataMap.data.length) {
    return null;
  }
  const ecgDataEntry = ecgDataMap.data[hourIndex];
  const urlECG = `${ecgDataEntry.dataUrl}?${ecgDataMap.querySignature}`;
  const fileData = await downloadFile(urlECG, CACHE_ECG);
  console.log('Downloaded ECG data for CACHE_ECG:', CACHE_ECG, );
  if (!fileData) return null;

  return {
    start: ecgDataEntry.start,
    stop: ecgDataEntry.stop,
    data: fileData,
  };
};
const getFileIndexByTimeFirstBeat = (startTime, endTime) => {
  for (let i = 0; i < studySummariesJson.length; i += 1) {
    const study = studySummariesJson[i];
    const studyStartTime = dayjs(study.start);
    const studyEndTime = dayjs(study.stop);
    if (dayjs(startTime).isBetween(studyStartTime, studyEndTime, null, '[]')
        || dayjs(endTime).isBetween(studyStartTime, studyEndTime, null, '[]')) {
      return i;
    }
  }
  return -1; // Not found
};


const getFileIndexByTime = (time, ecgDataMap) => {
  for (let i = 0; i < ecgDataMap.data.length; i += 1) {
    const entry = ecgDataMap.data[i];
    const startTime = dayjs(entry.start.$d);
    const stopTime = dayjs(entry.stop.$d);
    if (dayjs(time).isBetween(startTime, stopTime, null, '[]')) {
      return i;
    }
  }
  return -1;
};

const manageECGCache = async (currentHourIndex, ecgDataMap) => {
  const hoursToDownload = [currentHourIndex - 1, currentHourIndex, currentHourIndex + 1];
  const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

  // Download the required ECG files
  await Promise.all(validHours.map(hour => getECGDataForHour(hour, ecgDataMap)));

  if (CACHE_ECG.size > 3) {
    const keysToDelete = Array.from(CACHE_ECG.keys())
      .filter(key => !validHours.includes(parseInt(key.split('data-hourly-')[1].split('-')[0], 10)))
      .sort();

    // Delete entries until cache size is 3 or less
    keysToDelete.slice(0, CACHE_ECG.size - 3).forEach(key => CACHE_ECG.delete(key));
  }
};


const downloadECGDataByBeatTime = async (beatTime, ecgDataMap) => {
  try {
    // debugger;
    const beatTimestamp = dayjs(beatTime).valueOf();
    const fileIndex = getFileIndexByTime(beatTimestamp, ecgDataMap);

    if (fileIndex === -1) {
      throw new Error('No file found for the given beat time');
    }

    await manageECGCache(fileIndex, ecgDataMap);

    const hoursToDownload = [fileIndex - 1, fileIndex, fileIndex + 1];
    const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

    const ecgDataPromises = validHours.map(hour => getECGDataForHour(hour, ecgDataMap));
    const ecgDataResults = await Promise.all(ecgDataPromises);

    const ecgDataList = ecgDataResults.filter(ecgData => ecgData !== null);

    return ecgDataList;
  } catch (error) {
    console.error('Failed to download ECG data by beat time: ', error);
    return [];
  }
};


const mergeBeatData = (beatData, beatEpoch, offset) => {
  const beatMarkers = [];
  const hesBeatStatus = [];

  if (beatData) {
    beatData.beatEpochs.forEach((epoch, index) => {
      if (epoch >= (beatEpoch - offset) && epoch <= (beatEpoch + offset)) {
        const sample = Math.round(
          ((epoch - (beatEpoch - offset)) * config.samplingFrequency) / 1000,
        );
        beatMarkers.push(sample);
        hesBeatStatus.push(beatData.hesBeatStatus[index]);
      }
    });
  }

  return { beatMarkers, hesBeatStatus };
};

const downloadSelectedEcgData = async (ranges, querySignature) => {
  try {
    if (ranges.length) {
      const blobPromises = ranges.map(async (range) => {
        const cacheKey = `${range.dataUrl}?${querySignature}`;
        const filename = extractFilename(cacheKey);
        if (CACHE_ECG.has(filename)) {
          const cachedData = CACHE_ECG.get(filename);
          if (range.dataRange.start >= 0 && range.dataRange.stop <= cachedData.byteLength) {
            return cachedData.slice(range.dataRange.start, range.dataRange.stop + 1);
          }
          console.error('Invalid range:', range.dataRange);
          return null;
        }
        console.error('No cached data for URL:', range.dataUrl);
        return null;
      });

      const blob = (await Promise.all(blobPromises)).filter(data => data !== null);
      let concatenated = await new Blob(blob).arrayBuffer();
      const { byteLength } = concatenated;
      if (byteLength % 2 !== 0) {
        concatenated = concatenated.slice(0, byteLength - 1);
      }

      return new Int16Array(concatenated);
    }
    return [];
  } catch (error) {
    console.error('Failed to download selected ECG data: ', error);
    return [];
  }
};


const convertBinaryToECGData = (totalChannel, binaryData) => {
  if (!binaryData) {
    return undefined;
  }
  const ecgData = Array.from({ length: totalChannel }, () => []);
  for (let i = 0; i < binaryData.length; i += totalChannel) {
    for (let j = 0; j < totalChannel; j++) {
      ecgData[j].push(binaryData[i + j]);
    }
  }
  return ecgData;
};

const getSineWaveDataArray = (diffMilliseconds, samplingFrequency, adcGain) => {
  const halfSamplingFrequency = samplingFrequency / 2;
  const sineWaveCount = diffMilliseconds / 1000;

  const sineWaveDataArray = _.map(_.range(samplingFrequency), (x, i) => {
    const sample = Math.sin(x * ((2 * Math.PI) / samplingFrequency)) * (0.75 * (adcGain / 2));
    return i <= halfSamplingFrequency ? Math.round(sample - (1.25 * adcGain)) : Math.round(sample + (1.25 * adcGain));
  });
  let resultSineWave = [];
  const wholeSineWaveCount = Math.floor(sineWaveCount);
  const fractionalSineWaveCount = sineWaveCount - wholeSineWaveCount;
  for (let k = 0; k < wholeSineWaveCount; k += 1) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray);
  }
  if (fractionalSineWaveCount) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray.slice(0, Math.round(fractionalSineWaveCount * sineWaveDataArray.length)));
  }
  return resultSineWave;
};

const combineRanges = (ranges, start, stop) => {
  const adjustedRanges = ranges
    .map((subarray) => {
      let [subStart, subStop] = subarray;
      subStart = Math.max(subStart, start);
      subStop = Math.min(subStop, stop);
      return subStart <= subStop ? [subStart, subStop] : null;
    })
    .filter(subarray => subarray !== null);

  if (start <= stop) {
    adjustedRanges.unshift([start, start]);
    adjustedRanges.push([stop, stop]);
  }

  return adjustedRanges;
};


const addSineWaveToEcgData = (selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength = 75000) => {
  if (selectedEcgData[0].length === dataLength) {
    return selectedEcgData;
  }
  const { samplingFrequency, gain } = ecgDataMap;
  const ecgData = selectedEcgData.map(() => []);
  const rangesEpochs = ranges.map(x => [dayjs(x.start.$d).valueOf(), dayjs(x.stop.$d).valueOf()]);
  const startEpoch = dayjs(startMoment).valueOf();
  const stopEpoch = dayjs(stopMoment).valueOf();
  const rangesData = combineRanges(rangesEpochs, startEpoch, stopEpoch);
  let dataIndex = 0;
  rangesData.forEach((range, i) => {
    if (i > 0) {
      const currStart = range[0];
      const currStop = range[1];
      const prevStop = rangesData[i - 1][1];
      if (currStart - prevStop > 0) {
        const sineWave = getSineWaveDataArray(currStart - prevStop, samplingFrequency, gain);
        ecgData.forEach((data) => {
          data.push(...sineWave);
        });
      }
      if (currStop - currStart > 0) {
        const diffMs = (currStop - currStart) / 1000;
        const dataLength = Math.round(diffMs * samplingFrequency);
        ecgData.forEach((data, j) => {
          data.push(...selectedEcgData[j].slice(dataIndex, dataIndex + dataLength));
        });
        dataIndex += dataLength;
      }
    }
  });
  return ecgData;
};

const getSizeData = (stop, start, bytesPerSecond) => {
  const diffTime = +stop - +start;
  return diffTime * bytesPerSecond;
};

const addDataRanges = ({
  summaries, start: startEvent, stop: stopEvent, samplingFrequency, channels,
}) => {
  const bytesPerSample = 2;
  const samplePerMsSecond = channels.length * bytesPerSample * samplingFrequency / 1000;
  const lenHourlySummaries = summaries.length;
  for (let i = 0; i < lenHourlySummaries; i += 1) {
    const hourlySummary = summaries[i];
    const { start: startHs, stop: stopHs } = hourlySummary;
    const _startHs = dayjs(startHs.$d);
    const _stopHs = dayjs(stopHs.$d);
    const sizeData = getSizeData(
      Math.min(_stopHs, stopEvent),
      Math.max(_startHs, startEvent),
      samplePerMsSecond,
    );
    const readPosition = i === 0
      ? getSizeData(startEvent, Math.min(_startHs, startEvent), samplePerMsSecond)
      : 0;
    hourlySummary.dataRange = {
      start: Math.ceil(readPosition),
      stop: Math.floor(readPosition + sizeData),
    };
  }
};

const filterHourlySummaryRanges = ({
  summaries, start, stop, samplingFrequency, channels, willAddDataRanges,
}) => {
  const ranges = summaries.data.filter(summary => _.every([
    +dayjs(summary.stop.$d) > +start,
    +dayjs(summary.start.$d) < +stop,
  ]));
  if (willAddDataRanges) {
    addDataRanges({
      summaries: ranges,
      start,
      stop,
      samplingFrequency,
      channels,
    });
  }
  return ranges;
};

const handleDownloadData = async ({
  ecgDataMap, beat, offset, momentObject = {}, dataLength, config,
}) => {
  const { customOffset } = config || {};
  let epoch = beat?.start ? new Date(beat.start).getTime() : beat?.id;
  const dividedNumber = 1000 / ecgDataMap.samplingFrequency;
  epoch = Math.floor(epoch / dividedNumber) * dividedNumber;
  let startMoment;
  let stopMoment;
  if (!_.isEmpty(momentObject)) {
    startMoment = momentObject.startMoment;
    stopMoment = momentObject.stopMoment;
  } else if (!_.isEmpty(customOffset)) {
    startMoment = dayjs(epoch - customOffset[0]);
    stopMoment = dayjs(epoch + customOffset[1]);
  } else if (offset) {
    startMoment = dayjs(epoch - offset);
    stopMoment = dayjs(epoch + offset);
  }
  const { channels, samplingFrequency } = ecgDataMap;
  const ranges = filterHourlySummaryRanges({
    summaries: ecgDataMap,
    start: startMoment,
    stop: stopMoment,
    channels,
    samplingFrequency,
    willAddDataRanges: true,
  });

  try {
    const downloadedSelectedEcgData = await downloadSelectedEcgData(ranges, ecgDataMap.querySignature);
    if (downloadedSelectedEcgData?.length) {
      const selectedEcgData = convertBinaryToECGData(channels.length, downloadedSelectedEcgData);
      const addedSineWaveSelectedEcgData = addSineWaveToEcgData(selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength);
      return addedSineWaveSelectedEcgData;
    }
    throw new Error('Error');
  } catch (error) {
    throw error;
  }
};

const processChunkedArray = (chunkedArray, start, fileIndex) => {
  const beatData = {
    start,
    stop: studySummariesJson[fileIndex].stop,
    channel: null,
    beatEpochs: [],
    hesBeatStatus: [],
  };

  chunkedArray.forEach((item) => {
    const channel = item[0];
    const hesBeatStatus = item.slice(1, 2);
    const beatBytes = item.slice(2, 5).map(byte => (byte < 0 ? 256 + byte : byte));

    const beatPosition = (beatBytes[0] << 16) + (beatBytes[1] << 8) + beatBytes[2];
    const beatEpoch = dayjs(start).add(beatPosition / config.samplingFrequency, 'second').valueOf();
    beatData.channel = channel;
    beatData.beatEpochs.push(beatEpoch);
    beatData.hesBeatStatus.push(hesBeatStatus);
  });
  // Giải phóng bộ nhớ sau khi xử lý
  chunkedArray.length = 0;

  return beatData;
};

const getAdjacentFileBeatData = async (fileIndex) => {
  if (fileIndex < 0 || fileIndex >= fileUrls.length) {
    return null;
  }
  const fileUrl = fileUrls[fileIndex];
  const fileData = await downloadFile(fileUrl, CACHE_BEAT);
  if (!fileData) return null;

  const array = new Int8Array(fileData);
  const chunkedArray = chunk(array, 5);
  const { start } = studySummariesJson[fileIndex];
  const beatData = processChunkedArray(chunkedArray, start, fileIndex);
  return beatData;
};

// Hàm xử lý beat
const processBeatData = async (beatData, beatEpoch, offset, ecgDataMap) => {
  let beatMarkers = [];
  let hesBeatStatus = [];
  let ecgData = [];

  const currentData = mergeBeatData(beatData, beatEpoch, offset);
  beatMarkers = beatMarkers.concat(currentData.beatMarkers);
  hesBeatStatus = hesBeatStatus.concat(currentData.hesBeatStatus);
  if(offset = 2000) {
    ecgData = ecgData.concat(await handleDownloadData({
      ecgDataMap,
      beat: { id: beatEpoch },
      offset: offset,
      dataLength: ecgDataMap.samplingFrequency * 2,
    }));
  }
  return { beatMarkers, hesBeatStatus, ecgData };
};
// xử lý beat đầu hoặc cuối giờ
const getPreviousNextBeatData = async (currentIndex, allBeatData, offsets, beatEpoch, ecgDataMap) => {
  let previousBeatData = null;
  let nextBeatData = null;

  console.log("currentIndex:", currentIndex);
  console.log("allBeatData length:", allBeatData.length);

  if (currentIndex > 0) {
    console.log("Processing previous beat data");
    previousBeatData = await processBeatData(allBeatData[currentIndex - 1], beatEpoch, offsets, ecgDataMap);
  }

  if (currentIndex < allBeatData.length - 1) {
    console.log("Processing next beat data");
    nextBeatData = await processBeatData(allBeatData[currentIndex + 1], beatEpoch, offsets, ecgDataMap);
  }

  return { previousBeatData, nextBeatData };
};

expose({
  createConfig: (initialData) => {
    const { samplingFrequency , ecgDataMap } = initialData;
    config.samplingFrequency = samplingFrequency;
    config.ecgDataMap = ecgDataMap;
  },
  downloadFirstBeatList: async (startTime, endTime, offset, ecgDataMap, beatEpoch) => {
    try {
      const fileIndex = getFileIndexByTimeFirstBeat(startTime, endTime);
      if (fileIndex === -1) {
        throw new Error('No file found for the given time range');
      }

      const currentBeatData = await getAdjacentFileBeatData(fileIndex);
      const previousBeatData = fileIndex > 0 ? await getAdjacentFileBeatData(fileIndex - 1) : null;
      const nextBeatData = fileIndex < fileUrls.length - 1 ? await getAdjacentFileBeatData(fileIndex + 1) : null;
      let beatMarkers = [];
      let hesBeatStatus = [];


      // Merge data for the current beatEpoch
      const currentData = mergeBeatData(currentBeatData, beatEpoch, offset);
      beatMarkers = beatMarkers.concat(currentData.beatMarkers);
      hesBeatStatus = hesBeatStatus.concat(currentData.hesBeatStatus);

      // Merge data from previous file if available
      if (previousBeatData) {
        const previousData = mergeBeatData(previousBeatData, beatEpoch, offset);
        beatMarkers = beatMarkers.concat(previousData.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(previousData.hesBeatStatus);
      }

      // Merge data from next file if available
      if (nextBeatData) {
        const nextData = mergeBeatData(nextBeatData, beatEpoch, offset);
        beatMarkers = beatMarkers.concat(nextData.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(nextData.hesBeatStatus);
      }

      const result = {
        beatEpochs: beatEpoch,
        beatMarkers,
        hesBeatStatus,
      };
      postMessage({ result });
      return result;
    } catch (error) {
      console.error('Failed to download first beat: ', error);
      return null;
    } finally {
      self.close();
    }
  },
   downloadBeatListByDay : async () => {
    const offset_2s = 2000;
    const offset_10s = 10000;
    const ecgDataMap = config.ecgDataMap;
  
    try {
      let dailyBeatData = [];
      let allBeatData = await Promise.all(fileUrls.map((url, i) => getAdjacentFileBeatData(i)));
  
      for (let i = 0; i < allBeatData.length; i += 1) {
        const currentBeatData = allBeatData[i];
        await downloadECGDataByBeatTime(currentBeatData.start, ecgDataMap);
  
        const { start, stop } = studySummariesJson[i];
        const startDate = new Date(start);
        const stopDate = new Date(stop);
  
        let dateEntry = dailyBeatData.find(entry => entry.date === startDate.toISOString().split('T')[0]);
        if (!dateEntry) {
          dateEntry = { date: startDate.toISOString().split('T')[0], hours: [] };
          dailyBeatData.push(dateEntry);
        }
  
        let hourEntry = dateEntry.hours.find(entry => entry.hour === startDate.getHours());
        if (!hourEntry) {
          hourEntry = { hour: startDate.getHours(), value: [] };
          dateEntry.hours.push(hourEntry);
        }
  
        for (const beatEpoch of currentBeatData.beatEpochs) {
          let { beatMarkers: beatMarkers2s, hesBeatStatus: hesBeatStatus2s, ecgData: ecgData2s } = await processBeatData(currentBeatData, beatEpoch, offset_2s, ecgDataMap);
  
          const { previousBeatData: previousBeatData2s, nextBeatData: nextBeatData2s } = await getPreviousNextBeatData(i, allBeatData, offset_2s, beatEpoch, ecgDataMap);
  
          if (previousBeatData2s) {
            beatMarkers2s = beatMarkers2s.concat(previousBeatData2s.beatMarkers);
            hesBeatStatus2s = hesBeatStatus2s.concat(previousBeatData2s.hesBeatStatus);
          }
  
          if (nextBeatData2s) {
            beatMarkers2s = beatMarkers2s.concat(nextBeatData2s.beatMarkers);
            hesBeatStatus2s = hesBeatStatus2s.concat(nextBeatData2s.hesBeatStatus);
          }
  
          // Handling additional offset_10s
          let { beatMarkers: beatMarkers10s, hesBeatStatus: hesBeatStatus10s, ecgData: ecgData10s } = await processBeatData(currentBeatData, beatEpoch, offset_10s, ecgDataMap);
          
          const { previousBeatData: previousBeatData10s, nextBeatData: nextBeatData10s } = await getPreviousNextBeatData(i, allBeatData, offset_10s, beatEpoch, ecgDataMap);
  
          if (previousBeatData10s) {
            beatMarkers10s = beatMarkers10s.concat(previousBeatData10s.beatMarkers);
            hesBeatStatus10s = hesBeatStatus10s.concat(previousBeatData10s.hesBeatStatus);
          }
  
          if (nextBeatData10s) {
            beatMarkers10s = beatMarkers10s.concat(nextBeatData10s.beatMarkers);
            hesBeatStatus10s = hesBeatStatus10s.concat(nextBeatData10s.hesBeatStatus);
          }
  
          hourEntry.value.push({
            beatEpochs: beatEpoch,
            beat2s: {
              beatMarkers: beatMarkers2s,
              hesBeatStatus: hesBeatStatus2s,
              ecgData: ecgData2s,
            },
            beat10s: {
              beatMarkers: beatMarkers10s,
              hesBeatStatus: hesBeatStatus10s,
              ecgData: [],
            }
          });
        }
  
        const { date } = dateEntry;
        self.postMessage({ date, hourEntry });
        hourEntry = null;
        dateEntry = null;
      }
  
      dailyBeatData = null;
      allBeatData = null;
      CACHE_ECG.clear();
      CACHE_BEAT.clear();
      return 'done';
    } catch (error) {
      console.error('Failed to download beat data by day: ', error);
      return [];
    } finally {
      self.close();
    }
  },
  
});
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
import axios from 'axios';
import { expose } from 'threads/worker';
import _, { chunk } from 'lodash';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
import TTLCache from '@isaacs/ttlcache';
import studySummariesJson from '../../../../dummyData/studySummaries.json';

dayjs.extend(isBetween);


const fileUrls = [
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-08-08-14%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-09-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-10-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-11-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-12-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-13-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-14-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-15-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-16-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-17-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-18-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-19-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-20-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-21-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-22-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-23-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-00-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-01-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-02-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-03-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-04-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-05-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-06-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-07-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-08-00-00%2B28-final.beat',
];

const CACHE_ECG = new TTLCache({ ttl: 180000 }); // Cache ECG files for 3 minutes
const CACHE_BEAT = new TTLCache({ ttl: 180000 }); // Cache BEAT files for 3 minutes
const config = {};
const ecgDataMap = config.ecgDataMap;
const offset_2s = 2000;
const offset_10s = 10000;

function extractFilename(url) {
  const urlObj = new URL(url);
  const { pathname } = urlObj;
  const regex = /[^/]+$/;
  const match = pathname.match(regex);

  return match ? match[0] : null;
}

const downloadFile = async (url, cache) => {
  const filename = extractFilename(url);
  if (cache.has(filename)) {
    return cache.get(filename);
  }
  try {
    const response = await axios({
      method: 'GET',
      url,
      responseType: 'arraybuffer',
    });

    cache.set(filename, response.data);
    return response.data;
  } catch (error) {
    console.error('Failed to download file: ', error);
    return null;
  }
};

const getECGDataForHour = async (hourIndex, ecgDataMap) => {
  if (hourIndex < 0 || hourIndex >= ecgDataMap.data.length) {
    return null;
  }
  const ecgDataEntry = ecgDataMap.data[hourIndex];
  const urlECG = `${ecgDataEntry.dataUrl}?${ecgDataMap.querySignature}`;
  const fileData = await downloadFile(urlECG, CACHE_ECG);
  console.log('Downloaded ECG data for CACHE_ECG:', CACHE_ECG);
  if (!fileData) return null;

  return {
    start: ecgDataEntry.start,
    stop: ecgDataEntry.stop,
    data: fileData,
  };
};

const getFileIndexByTimeFirstBeat = (startTime, endTime) => {
  for (let i = 0; i < studySummariesJson.length; i += 1) {
    const study = studySummariesJson[i];
    const studyStartTime = dayjs(study.start);
    const studyEndTime = dayjs(study.stop);
    if (dayjs(startTime).isBetween(studyStartTime, studyEndTime, null, '[]')
      || dayjs(endTime).isBetween(studyStartTime, studyEndTime, null, '[]')) {
      return i;
    }
  }
  return -1; // Not found
};

const getFileIndexByTime = (time, ecgDataMap) => {
  for (let i = 0; i < ecgDataMap.data.length; i += 1) {
    const entry = ecgDataMap.data[i];
    const startTime = dayjs(entry.start.$d);
    const stopTime = dayjs(entry.stop.$d);
    if (dayjs(time).isBetween(startTime, stopTime, null, '[]')) {
      return i;
    }
  }
  return -1;
};

const manageECGCache = async (currentHourIndex, ecgDataMap) => {
  const hoursToDownload = [currentHourIndex - 1, currentHourIndex, currentHourIndex + 1];
  const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

  validHours.forEach(hour => getECGDataForHour(hour, ecgDataMap));

  if (CACHE_ECG.size > 3) {
    const keysToDelete = Array.from(CACHE_ECG.keys())
      .filter(key => !validHours.includes(parseInt(key.split('data-hourly-')[1].split('-')[0], 10)))
      .sort();

    keysToDelete.slice(0, CACHE_ECG.size - 3).forEach(key => CACHE_ECG.delete(key));
  }
};

const downloadECGDataByBeatTime = async (beatTime, ecgDataMap) => {
  try {
    const beatTimestamp = dayjs(beatTime).valueOf();
    const fileIndex = getFileIndexByTime(beatTimestamp, ecgDataMap);

    if (fileIndex === -1) {
      throw new Error('No file found for the given beat time');
    }

    manageECGCache(fileIndex, ecgDataMap);

    const hoursToDownload = [fileIndex - 1, fileIndex, fileIndex + 1];
    const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

    const ecgDataPromises = validHours.map(hour => getECGDataForHour(hour, ecgDataMap));
    const ecgDataResults = await Promise.all(ecgDataPromises);

    const ecgDataList = ecgDataResults.filter(ecgData => ecgData !== null);

    return ecgDataList;
  } catch (error) {
    console.error('Failed to download ECG data by beat time: ', error);
    return [];
  }
};

const mergeBeatData = (beatData, beatEpoch, offset) => {
  const beatMarkers = [];
  const hesBeatStatus = [];

  if (beatData) {
    beatData.beatEpochs.forEach((epoch, index) => {
      if (epoch >= (beatEpoch - offset) && epoch <= (beatEpoch + offset)) {
        const sample = Math.round(
          ((epoch - (beatEpoch - offset)) * config.samplingFrequency) / 1000,
        );
        beatMarkers.push(sample);
        hesBeatStatus.push(beatData.hesBeatStatus[index]);
      }
    });
  }

  return { beatMarkers, hesBeatStatus };
};

const downloadSelectedEcgData = async (ranges, querySignature) => {
  try {
    if (ranges.length) {
      const blobPromises = ranges.map(async (range) => {
        const cacheKey = `${range.dataUrl}?${querySignature}`;
        const filename = extractFilename(cacheKey);
        if (CACHE_ECG.has(filename)) {
          const cachedData = CACHE_ECG.get(filename);
          if (range.dataRange.start >= 0 && range.dataRange.stop <= cachedData.byteLength) {
            return cachedData.slice(range.dataRange.start, range.dataRange.stop + 1);
          }
          console.error('Invalid range:', range.dataRange);
          return null;
        }
        console.error('No cached data for URL:', range.dataUrl);
        return null;
      });

      const blob = (await Promise.all(blobPromises)).filter(data => data !== null);
      let concatenated = await new Blob(blob).arrayBuffer();
      const { byteLength } = concatenated;
      if (byteLength % 2 !== 0) {
        concatenated = concatenated.slice(0, byteLength - 1);
      }

      return new Int16Array(concatenated);
    }
    return [];
  } catch (error) {
    console.error('Failed to download selected ECG data: ', error);
    return [];
  }
};

const convertBinaryToECGData = (totalChannel, binaryData) => {
  if (!binaryData) {
    return undefined;
  }
  const ecgData = Array.from({ length: totalChannel }, () => []);
  for (let i = 0; i < binaryData.length; i += totalChannel) {
    for (let j = 0; j < totalChannel; j++) {
      ecgData[j].push(binaryData[i + j]);
    }
  }
  return ecgData;
};

const getSineWaveDataArray = (diffMilliseconds, samplingFrequency, adcGain) => {
  const halfSamplingFrequency = samplingFrequency / 2;
  const sineWaveCount = diffMilliseconds / 1000;

  const sineWaveDataArray = _.map(_.range(samplingFrequency), (x, i) => {
    const sample = Math.sin(x * ((2 * Math.PI) / samplingFrequency)) * (0.75 * (adcGain / 2));
    return i <= halfSamplingFrequency ? Math.round(sample - (1.25 * adcGain)) : Math.round(sample + (1.25 * adcGain));
  });
  let resultSineWave = [];
  const wholeSineWaveCount = Math.floor(sineWaveCount);
  const fractionalSineWaveCount = sineWaveCount - wholeSineWaveCount;
  for (let k = 0; k < wholeSineWaveCount; k += 1) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray);
  }
  if (fractionalSineWaveCount) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray.slice(0, Math.round(fractionalSineWaveCount * sineWaveDataArray.length)));
  }
  return resultSineWave;
};

const combineRanges = (ranges, start, stop) => {
  const adjustedRanges = ranges
    .map((subarray) => {
      let [subStart, subStop] = subarray;
      subStart = Math.max(subStart, start);
      subStop = Math.min(subStop, stop);
      return subStart <= subStop ? [subStart, subStop] : null;
    })
    .filter(subarray => subarray !== null);

  if (start <= stop) {
    adjustedRanges.unshift([start, start]);
    adjustedRanges.push([stop, stop]);
  }

  return adjustedRanges;
};

const addSineWaveToEcgData = (selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength = 75000) => {
  if (selectedEcgData[0].length === dataLength) {
    return selectedEcgData;
  }
  const { samplingFrequency, gain } = ecgDataMap;
  const ecgData = selectedEcgData.map(() => []);
  const rangesEpochs = ranges.map(x => [dayjs(x.start.$d).valueOf(), dayjs(x.stop.$d).valueOf()]);
  const startEpoch = dayjs(startMoment).valueOf();
  const stopEpoch = dayjs(stopMoment).valueOf();
  const rangesData = combineRanges(rangesEpochs, startEpoch, stopEpoch);
  let dataIndex = 0;
  rangesData.forEach((range, i) => {
    if (i > 0) {
      const currStart = range[0];
      const currStop = range[1];
      const prevStop = rangesData[i - 1][1];
      if (currStart - prevStop > 0) {
        const sineWave = getSineWaveDataArray(currStart - prevStop, samplingFrequency, gain);
        ecgData.forEach((data) => {
          data.push(...sineWave);
        });
      }
      if (currStop - currStart > 0) {
        const diffMs = (currStop - currStart) / 1000;
        const dataLength = Math.round(diffMs * samplingFrequency);
        ecgData.forEach((data, j) => {
          data.push(...selectedEcgData[j].slice(dataIndex, dataIndex + dataLength));
        });
        dataIndex += dataLength;
      }
    }
  });
  return ecgData;
};

const getSizeData = (stop, start, bytesPerSecond) => {
  const diffTime = +stop - +start;
  return diffTime * bytesPerSecond;
};

const addDataRanges = ({
  summaries, start: startEvent, stop: stopEvent, samplingFrequency, channels,
}) => {
  const bytesPerSample = 2;
  const samplePerMsSecond = channels.length * bytesPerSample * samplingFrequency / 1000;
  const lenHourlySummaries = summaries.length;
  for (let i = 0; i < lenHourlySummaries; i += 1) {
    const hourlySummary = summaries[i];
    const { start: startHs, stop: stopHs } = hourlySummary;
    const _startHs = dayjs(startHs.$d);
    const _stopHs = dayjs(stopHs.$d);
    const sizeData = getSizeData(
      Math.min(_stopHs, stopEvent),
      Math.max(_startHs, startEvent),
      samplePerMsSecond,
    );
    const readPosition = i === 0
      ? getSizeData(startEvent, Math.min(_startHs, startEvent), samplePerMsSecond)
      : 0;
    hourlySummary.dataRange = {
      start: Math.ceil(readPosition),
      stop: Math.floor(readPosition + sizeData),
    };
  }
};

const filterHourlySummaryRanges = ({
  summaries, start, stop, samplingFrequency, channels, willAddDataRanges,
}) => {
  const ranges = summaries.data.filter(summary => _.every([
    +dayjs(summary.stop.$d) > +start,
    +dayjs(summary.start.$d) < +stop,
  ]));
  if (willAddDataRanges) {
    addDataRanges({
      summaries: ranges,
      start,
      stop,
      samplingFrequency,
      channels,
    });
  }
  return ranges;
};

const handleDownloadData = async ({
  ecgDataMap, beat, offset, momentObject = {}, dataLength, config,
}) => {
  const { customOffset } = config || {};
  let epoch = beat?.start ? new Date(beat.start).getTime() : beat?.id;
  const dividedNumber = 1000 / ecgDataMap.samplingFrequency;
  epoch = Math.floor(epoch / dividedNumber) * dividedNumber;
  let startMoment;
  let stopMoment;
  if (!_.isEmpty(momentObject)) {
    startMoment = momentObject.startMoment;
    stopMoment = momentObject.stopMoment;
  } else if (!_.isEmpty(customOffset)) {
    startMoment = dayjs(epoch - customOffset[0]);
    stopMoment = dayjs(epoch + customOffset[1]);
  } else if (offset) {
    startMoment = dayjs(epoch - offset);
    stopMoment = dayjs(epoch + offset);
  }
  const { channels, samplingFrequency } = ecgDataMap;
  const ranges = filterHourlySummaryRanges({
    summaries: ecgDataMap,
    start: startMoment,
    stop: stopMoment,
    channels,
    samplingFrequency,
    willAddDataRanges: true,
  });

  try {
    const downloadedSelectedEcgData = await downloadSelectedEcgData(ranges, ecgDataMap.querySignature);
    if (downloadedSelectedEcgData?.length) {
      const selectedEcgData = convertBinaryToECGData(channels.length, downloadedSelectedEcgData);
      const addedSineWaveSelectedEcgData = addSineWaveToEcgData(selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength);
      return addedSineWaveSelectedEcgData;
    }
    throw new Error('Error');
  } catch (error) {
    throw error;
  }
};

const processChunkedArray = (chunkedArray, start, fileIndex) => {
  const beatData = {
    start,
    stop: studySummariesJson[fileIndex].stop,
    channel: null,
    beatEpochs: [],
    hesBeatStatus: [],
  };

  chunkedArray.forEach((item) => {
    const channel = item[0];
    const hesBeatStatus = item.slice(1, 2);
    const beatBytes = item.slice(2, 5).map(byte => (byte < 0 ? 256 + byte : byte));

    const beatPosition = (beatBytes[0] << 16) + (beatBytes[1] << 8) + beatBytes[2];
    const beatEpoch = dayjs(start).add(beatPosition / config.samplingFrequency, 'second').valueOf();
    beatData.channel = channel;
    beatData.beatEpochs.push(beatEpoch);
    beatData.hesBeatStatus.push(hesBeatStatus);
  });
  chunkedArray.length = 0;

  return beatData;
};

const getAdjacentFileBeatData = async (fileIndex) => {
  if (fileIndex < 0 || fileIndex >= fileUrls.length) {
    return null;
  }
  const fileUrl = fileUrls[fileIndex];
  const fileData = await downloadFile(fileUrl, CACHE_BEAT);
  if (!fileData) return null;

  const array = new Int8Array(fileData);
  const chunkedArray = chunk(array, 5);
  const { start } = studySummariesJson[fileIndex];
  const beatData = processChunkedArray(chunkedArray, start, fileIndex);
  return beatData;
};

const processBeatData = async (beatData, beatEpoch, offset, ecgDataMap) => {
  let beatMarkers = [];
  let hesBeatStatus = [];
  let ecgData = [];

  const currentData = mergeBeatData(beatData, beatEpoch, offset);
  beatMarkers = beatMarkers.concat(currentData.beatMarkers);
  hesBeatStatus = hesBeatStatus.concat(currentData.hesBeatStatus);
  if (offset === offset_2s) {
    ecgData = ecgData.concat(await handleDownloadData({
      ecgDataMap,
      beat: { id: beatEpoch },
      offset: offset,
      dataLength: ecgDataMap.samplingFrequency * 2,
    }));
  }
  return { beatMarkers, hesBeatStatus, ecgData };
};

const getPreviousNextBeatData = async (currentIndex, allBeatData, offsets, beatEpoch, ecgDataMap) => {
  let previousBeatData = null;
  let nextBeatData = null;

  if (currentIndex > 0) {
    previousBeatData = processBeatData(allBeatData[currentIndex - 1], beatEpoch, offsets, ecgDataMap);
  }

  if (currentIndex < allBeatData.length - 1) {
    nextBeatData = processBeatData(allBeatData[currentIndex + 1], beatEpoch, offsets, ecgDataMap);
  }

  return { previousBeatData, nextBeatData };
};

const handleBeatDataProcessing = async (currentBeatData, beatEpoch, ecgDataMap, i, allBeatData) => {
  let beatMarkers2s = [], hesBeatStatus2s = [], ecgData2s = [];
  let beatMarkers10s = [], hesBeatStatus10s = [], ecgData10s = [];

  const process2s = processBeatData(currentBeatData, beatEpoch, offset_2s, ecgDataMap)
    .then(data => {
      beatMarkers2s = data.beatMarkers;
      hesBeatStatus2s = data.hesBeatStatus;
      ecgData2s = data.ecgData;
    });

  const process10s = processBeatData(currentBeatData, beatEpoch, offset_10s, ecgDataMap)
    .then(data => {
      beatMarkers10s = data.beatMarkers;
      hesBeatStatus10s = data.hesBeatStatus;
      ecgData10s = data.ecgData;
    });

  const processPreviousNext2s = getPreviousNextBeatData(i, allBeatData, offset_2s, beatEpoch, ecgDataMap)
    .then(data => {
      if (data.previousBeatData) {
        beatMarkers2s = beatMarkers2s.concat(data.previousBeatData.beatMarkers);
        hesBeatStatus2s = hesBeatStatus2s.concat(data.previousBeatData.hesBeatStatus);
      }

      if (data.nextBeatData) {
        beatMarkers2s = beatMarkers2s.concat(data.nextBeatData.beatMarkers);
        hesBeatStatus2s = hesBeatStatus2s.concat(data.nextBeatData.hesBeatStatus);
      }
    });

  const processPreviousNext10s = getPreviousNextBeatData(i, allBeatData, offset_10s, beatEpoch, ecgDataMap)
    .then(data => {
      if (data.previousBeatData) {
        beatMarkers10s = beatMarkers10s.concat(data.previousBeatData.beatMarkers);
        hesBeatStatus10s = hesBeatStatus10s.concat(data.previousBeatData.hesBeatStatus);
      }

      if (data.nextBeatData) {
        beatMarkers10s = beatMarkers10s.concat(data.nextBeatData.beatMarkers);
        hesBeatStatus10s = hesBeatStatus10s.concat(data.nextBeatData.hesBeatStatus);
      }
    });

  return Promise.all([process2s, process10s, processPreviousNext2s, processPreviousNext10s])
    .then(() => ({
      beatEpoch,
      beat2s: {
        beatMarkers: beatMarkers2s,
        hesBeatStatus: hesBeatStatus2s,
        ecgData: ecgData2s,
      },
      beat10s: {
        beatMarkers: beatMarkers10s,
        hesBeatStatus: hesBeatStatus10s,
        ecgData: [],
      }
    }));
};

expose({
  createConfig: (initialData) => {
    const { samplingFrequency, ecgDataMap } = initialData;
    config.samplingFrequency = samplingFrequency;
    config.ecgDataMap = ecgDataMap;
  },
  downloadFirstBeatList: async (startTime, endTime, offset, beatEpoch) => {
    try {
      const fileIndex = getFileIndexByTimeFirstBeat(startTime, endTime);
      if (fileIndex === -1) {
        throw new Error('No file found for the given time range');
      }

      const allBeatData = await Promise.all(fileUrls.map((url, i) => getAdjacentFileBeatData(i)));
      const currentBeatData = await getAdjacentFileBeatData(fileIndex);
      const previousBeatData = fileIndex > 0 ? await getAdjacentFileBeatData(fileIndex - 1) : null;
      const nextBeatData = fileIndex < fileUrls.length - 1 ? await getAdjacentFileBeatData(fileIndex + 1) : null;

      let beatMarkers = [];
      let hesBeatStatus = [];
      let ecgData = [];

      const currentData = mergeBeatData(currentBeatData, beatEpoch, offset_2s);
      beatMarkers = beatMarkers.concat(currentData.beatMarkers);
      hesBeatStatus = hesBeatStatus.concat(currentData.hesBeatStatus);

      if (previousBeatData) {
        const previousData = mergeBeatData(previousBeatData, beatEpoch, offset_2s);
        beatMarkers = beatMarkers.concat(previousData.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(previousData.hesBeatStatus);
      }

      if (nextBeatData) {
        const nextData = mergeBeatData(nextBeatData, beatEpoch, offset_2s);
        beatMarkers = beatMarkers.concat(nextData.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(nextData.hesBeatStatus);
      }

      const result = {
        beatEpochs: beatEpoch,
        beatMarkers,
        hesBeatStatus,
      };
      postMessage({ result });
      return result;
    } catch (error) {
      console.error('Failed to download first beat: ', error);
      return null;
    } finally {
      self.close();
    }
  },
  downloadBeatListByDay: async () => {
    try {
      let allBeatData = await Promise.all(fileUrls.map((url, i) => getAdjacentFileBeatData(i)));

      for (let i = 0; i < allBeatData.length; i += 1) {
        const currentBeatData = allBeatData[i];
        await downloadECGDataByBeatTime(currentBeatData.start, ecgDataMap);

        const { start, stop } = studySummariesJson[i];
        const startDate = new Date(start);
        const stopDate = new Date(stop);
        const date = startDate.toISOString().split('T')[0];
        const hour = startDate.getHours();

        currentBeatData.beatEpochs.forEach(async beatEpoch => {
          const hourData = await handleBeatDataProcessing(currentBeatData, beatEpoch, ecgDataMap, i, allBeatData);
          hourData.date = date;
          hourData.hour = hour;
          self.postMessage({ hourData });
        });
      }

      allBeatData = null;
      CACHE_ECG.clear();
      CACHE_BEAT.clear();
      return 'done';
    } catch (error) {
      console.error('Failed to download beat data by day: ', error);
      return [];
    } finally {
      self.close();
    }
  },
});
const functions = require('@google-cloud/functions-framework');

functions.http('helloHttp', (req, res) => {
 res.send(`Hello ${req.query.name || req.body.name || 'World'}!`);
});
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
import axios from 'axios';
import { expose } from 'threads/worker';
import _, { chunk } from 'lodash';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
import TTLCache from '@isaacs/ttlcache';
import studySummariesJson from '../../../../dummyData/studySummaries.json';

dayjs.extend(isBetween);


const fileUrls = [
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-08-08-14%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-09-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-10-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-11-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-12-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-13-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-14-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-15-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-16-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-17-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-18-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-19-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-20-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-21-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-22-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-23-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-00-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-01-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-02-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-03-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-04-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-05-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-06-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-07-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-08-00-00%2B28-final.beat',
];

const CACHE_ECG = new TTLCache({ ttl: 180000 }); // Cache ECG files for 3 minutes
const CACHE_BEAT = new TTLCache({ ttl: 180000 }); // Cache BEAT files for 3 minutes
const config = {};
const ecgDataMap = config.ecgDataMap;
const offset_2s = 2000;
const offset_10s = 10000;

function extractFilename(url) {
  const urlObj = new URL(url);
  const { pathname } = urlObj;
  const regex = /[^/]+$/;
  const match = pathname.match(regex);

  return match ? match[0] : null;
}

const downloadFile = async (url, cache) => {
  const filename = extractFilename(url);
  if (cache.has(filename)) {
    return cache.get(filename);
  }
  try {
    const response = await axios({
      method: 'GET',
      url,
      responseType: 'arraybuffer',
    });

    cache.set(filename, response.data);
    return response.data;
  } catch (error) {
    console.error('Failed to download file: ', error);
    return null;
  }
};

const getECGDataForHour = async (hourIndex, ecgDataMap) => {
  if (hourIndex < 0 || hourIndex >= ecgDataMap.data.length) {
    return null;
  }
  const ecgDataEntry = ecgDataMap.data[hourIndex];
  const urlECG = `${ecgDataEntry.dataUrl}?${ecgDataMap.querySignature}`;
  const fileData = await downloadFile(urlECG, CACHE_ECG);
  console.log('Downloaded ECG data for CACHE_ECG:', CACHE_ECG);
  if (!fileData) return null;

  return {
    start: ecgDataEntry.start,
    stop: ecgDataEntry.stop,
    data: fileData,
  };
};

const getFileIndexByTimeFirstBeat = (startTime, endTime) => {
  for (let i = 0; i < studySummariesJson.length; i += 1) {
    const study = studySummariesJson[i];
    const studyStartTime = dayjs(study.start);
    const studyEndTime = dayjs(study.stop);
    if (dayjs(startTime).isBetween(studyStartTime, studyEndTime, null, '[]')
      || dayjs(endTime).isBetween(studyStartTime, studyEndTime, null, '[]')) {
      return i;
    }
  }
  return -1; // Not found
};

const getFileIndexByTime = (time, ecgDataMap) => {
  for (let i = 0; i < ecgDataMap.data.length; i += 1) {
    const entry = ecgDataMap.data[i];
    const startTime = dayjs(entry.start.$d);
    const stopTime = dayjs(entry.stop.$d);
    if (dayjs(time).isBetween(startTime, stopTime, null, '[]')) {
      return i;
    }
  }
  return -1;
};

const manageECGCache = async (currentHourIndex, ecgDataMap) => {
  const hoursToDownload = [currentHourIndex - 1, currentHourIndex, currentHourIndex + 1];
  const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

  validHours.forEach(hour => getECGDataForHour(hour, ecgDataMap));

  if (CACHE_ECG.size > 3) {
    const keysToDelete = Array.from(CACHE_ECG.keys())
      .filter(key => !validHours.includes(parseInt(key.split('data-hourly-')[1].split('-')[0], 10)))
      .sort();

    keysToDelete.slice(0, CACHE_ECG.size - 3).forEach(key => CACHE_ECG.delete(key));
  }
};

const downloadECGDataByBeatTime = async (beatTime, ecgDataMap) => {
  try {
    const beatTimestamp = dayjs(beatTime).valueOf();
    const fileIndex = getFileIndexByTime(beatTimestamp, ecgDataMap);

    if (fileIndex === -1) {
      throw new Error('No file found for the given beat time');
    }

    manageECGCache(fileIndex, ecgDataMap);

    const hoursToDownload = [fileIndex - 1, fileIndex, fileIndex + 1];
    const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

    const ecgDataPromises = validHours.map(hour => getECGDataForHour(hour, ecgDataMap));
    const ecgDataResults = await Promise.all(ecgDataPromises);

    const ecgDataList = ecgDataResults.filter(ecgData => ecgData !== null);

    return ecgDataList;
  } catch (error) {
    console.error('Failed to download ECG data by beat time: ', error);
    return [];
  }
};

const mergeBeatData = (beatData, beatEpoch, offset) => {
  const beatMarkers = [];
  const hesBeatStatus = [];

  if (beatData) {
    beatData.beatEpochs.forEach((epoch, index) => {
      if (epoch >= (beatEpoch - offset) && epoch <= (beatEpoch + offset)) {
        const sample = Math.round(
          ((epoch - (beatEpoch - offset)) * config.samplingFrequency) / 1000,
        );
        beatMarkers.push(sample);
        hesBeatStatus.push(beatData.hesBeatStatus[index]);
      }
    });
  }

  return { beatMarkers, hesBeatStatus };
};

const downloadSelectedEcgData = async (ranges, querySignature) => {
  try {
    if (ranges.length) {
      const blobPromises = ranges.map(async (range) => {
        const cacheKey = `${range.dataUrl}?${querySignature}`;
        const filename = extractFilename(cacheKey);
        if (CACHE_ECG.has(filename)) {
          const cachedData = CACHE_ECG.get(filename);
          if (range.dataRange.start >= 0 && range.dataRange.stop <= cachedData.byteLength) {
            return cachedData.slice(range.dataRange.start, range.dataRange.stop + 1);
          }
          console.error('Invalid range:', range.dataRange);
          return null;
        }
        console.error('No cached data for URL:', range.dataUrl);
        return null;
      });

      const blob = (await Promise.all(blobPromises)).filter(data => data !== null);
      let concatenated = await new Blob(blob).arrayBuffer();
      const { byteLength } = concatenated;
      if (byteLength % 2 !== 0) {
        concatenated = concatenated.slice(0, byteLength - 1);
      }

      return new Int16Array(concatenated);
    }
    return [];
  } catch (error) {
    console.error('Failed to download selected ECG data: ', error);
    return [];
  }
};

const convertBinaryToECGData = (totalChannel, binaryData) => {
  if (!binaryData) {
    return undefined;
  }
  const ecgData = Array.from({ length: totalChannel }, () => []);
  for (let i = 0; i < binaryData.length; i += totalChannel) {
    for (let j = 0; j < totalChannel; j++) {
      ecgData[j].push(binaryData[i + j]);
    }
  }
  return ecgData;
};

const getSineWaveDataArray = (diffMilliseconds, samplingFrequency, adcGain) => {
  const halfSamplingFrequency = samplingFrequency / 2;
  const sineWaveCount = diffMilliseconds / 1000;

  const sineWaveDataArray = _.map(_.range(samplingFrequency), (x, i) => {
    const sample = Math.sin(x * ((2 * Math.PI) / samplingFrequency)) * (0.75 * (adcGain / 2));
    return i <= halfSamplingFrequency ? Math.round(sample - (1.25 * adcGain)) : Math.round(sample + (1.25 * adcGain));
  });
  let resultSineWave = [];
  const wholeSineWaveCount = Math.floor(sineWaveCount);
  const fractionalSineWaveCount = sineWaveCount - wholeSineWaveCount;
  for (let k = 0; k < wholeSineWaveCount; k += 1) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray);
  }
  if (fractionalSineWaveCount) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray.slice(0, Math.round(fractionalSineWaveCount * sineWaveDataArray.length)));
  }
  return resultSineWave;
};

const combineRanges = (ranges, start, stop) => {
  const adjustedRanges = ranges
    .map((subarray) => {
      let [subStart, subStop] = subarray;
      subStart = Math.max(subStart, start);
      subStop = Math.min(subStop, stop);
      return subStart <= subStop ? [subStart, subStop] : null;
    })
    .filter(subarray => subarray !== null);

  if (start <= stop) {
    adjustedRanges.unshift([start, start]);
    adjustedRanges.push([stop, stop]);
  }

  return adjustedRanges;
};

const addSineWaveToEcgData = (selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength = 75000) => {
  if (selectedEcgData[0].length === dataLength) {
    return selectedEcgData;
  }
  const { samplingFrequency, gain } = ecgDataMap;
  const ecgData = selectedEcgData.map(() => []);
  const rangesEpochs = ranges.map(x => [dayjs(x.start.$d).valueOf(), dayjs(x.stop.$d).valueOf()]);
  const startEpoch = dayjs(startMoment).valueOf();
  const stopEpoch = dayjs(stopMoment).valueOf();
  const rangesData = combineRanges(rangesEpochs, startEpoch, stopEpoch);
  let dataIndex = 0;
  rangesData.forEach((range, i) => {
    if (i > 0) {
      const currStart = range[0];
      const currStop = range[1];
      const prevStop = rangesData[i - 1][1];
      if (currStart - prevStop > 0) {
        const sineWave = getSineWaveDataArray(currStart - prevStop, samplingFrequency, gain);
        ecgData.forEach((data) => {
          data.push(...sineWave);
        });
      }
      if (currStop - currStart > 0) {
        const diffMs = (currStop - currStart) / 1000;
        const dataLength = Math.round(diffMs * samplingFrequency);
        ecgData.forEach((data, j) => {
          data.push(...selectedEcgData[j].slice(dataIndex, dataIndex + dataLength));
        });
        dataIndex += dataLength;
      }
    }
  });
  return ecgData;
};

const getSizeData = (stop, start, bytesPerSecond) => {
  const diffTime = +stop - +start;
  return diffTime * bytesPerSecond;
};

const addDataRanges = ({
  summaries, start: startEvent, stop: stopEvent, samplingFrequency, channels,
}) => {
  const bytesPerSample = 2;
  const samplePerMsSecond = channels.length * bytesPerSample * samplingFrequency / 1000;
  const lenHourlySummaries = summaries.length;
  for (let i = 0; i < lenHourlySummaries; i += 1) {
    const hourlySummary = summaries[i];
    const { start: startHs, stop: stopHs } = hourlySummary;
    const _startHs = dayjs(startHs.$d);
    const _stopHs = dayjs(stopHs.$d);
    const sizeData = getSizeData(
      Math.min(_stopHs, stopEvent),
      Math.max(_startHs, startEvent),
      samplePerMsSecond,
    );
    const readPosition = i === 0
      ? getSizeData(startEvent, Math.min(_startHs, startEvent), samplePerMsSecond)
      : 0;
    hourlySummary.dataRange = {
      start: Math.ceil(readPosition),
      stop: Math.floor(readPosition + sizeData),
    };
  }
};

const filterHourlySummaryRanges = ({
  summaries, start, stop, samplingFrequency, channels, willAddDataRanges,
}) => {
  const ranges = summaries.data.filter(summary => _.every([
    +dayjs(summary.stop.$d) > +start,
    +dayjs(summary.start.$d) < +stop,
  ]));
  if (willAddDataRanges) {
    addDataRanges({
      summaries: ranges,
      start,
      stop,
      samplingFrequency,
      channels,
    });
  }
  return ranges;
};

const handleDownloadData = async ({
  ecgDataMap, beat, offset, momentObject = {}, dataLength, config,
}) => {
  const { customOffset } = config || {};
  let epoch = beat?.start ? new Date(beat.start).getTime() : beat?.id;
  const dividedNumber = 1000 / ecgDataMap.samplingFrequency;
  epoch = Math.floor(epoch / dividedNumber) * dividedNumber;
  let startMoment;
  let stopMoment;
  if (!_.isEmpty(momentObject)) {
    startMoment = momentObject.startMoment;
    stopMoment = momentObject.stopMoment;
  } else if (!_.isEmpty(customOffset)) {
    startMoment = dayjs(epoch - customOffset[0]);
    stopMoment = dayjs(epoch + customOffset[1]);
  } else if (offset) {
    startMoment = dayjs(epoch - offset);
    stopMoment = dayjs(epoch + offset);
  }
  const { channels, samplingFrequency } = ecgDataMap;
  const ranges = filterHourlySummaryRanges({
    summaries: ecgDataMap,
    start: startMoment,
    stop: stopMoment,
    channels,
    samplingFrequency,
    willAddDataRanges: true,
  });

  try {
    const downloadedSelectedEcgData = await downloadSelectedEcgData(ranges, ecgDataMap.querySignature);
    if (downloadedSelectedEcgData?.length) {
      const selectedEcgData = convertBinaryToECGData(channels.length, downloadedSelectedEcgData);
      const addedSineWaveSelectedEcgData = addSineWaveToEcgData(selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength);
      return addedSineWaveSelectedEcgData;
    }
    throw new Error('Error');
  } catch (error) {
    throw error;
  }
};

const processChunkedArray = (chunkedArray, start, fileIndex) => {
  const beatData = {
    start,
    stop: studySummariesJson[fileIndex].stop,
    channel: null,
    beatEpochs: [],
    hesBeatStatus: [],
  };

  chunkedArray.forEach((item) => {
    const channel = item[0];
    const hesBeatStatus = item.slice(1, 2);
    const beatBytes = item.slice(2, 5).map(byte => (byte < 0 ? 256 + byte : byte));

    const beatPosition = (beatBytes[0] << 16) + (beatBytes[1] << 8) + beatBytes[2];
    const beatEpoch = dayjs(start).add(beatPosition / config.samplingFrequency, 'second').valueOf();
    beatData.channel = channel;
    beatData.beatEpochs.push(beatEpoch);
    beatData.hesBeatStatus.push(hesBeatStatus);
  });
  chunkedArray.length = 0;

  return beatData;
};

const getAdjacentFileBeatData = async (fileIndex) => {
  if (fileIndex < 0 || fileIndex >= fileUrls.length) {
    return null;
  }
  const fileUrl = fileUrls[fileIndex];
  const fileData = await downloadFile(fileUrl, CACHE_BEAT);
  if (!fileData) return null;

  const array = new Int8Array(fileData);
  const chunkedArray = chunk(array, 5);
  const { start } = studySummariesJson[fileIndex];
  const beatData = processChunkedArray(chunkedArray, start, fileIndex);
  return beatData;
};

const processBeatData = async (beatData, beatEpoch, offset, ecgDataMap) => {
  let beatMarkers = [];
  let hesBeatStatus = [];
  let ecgData = [];

  const currentData = mergeBeatData(beatData, beatEpoch, offset);
  beatMarkers = beatMarkers.concat(currentData.beatMarkers);
  hesBeatStatus = hesBeatStatus.concat(currentData.hesBeatStatus);
  if (offset === offset_2s) {
    ecgData = ecgData.concat(await handleDownloadData({
      ecgDataMap,
      beat: { id: beatEpoch },
      offset: offset,
      dataLength: ecgDataMap.samplingFrequency * 2,
    }));
  }
  return { beatMarkers, hesBeatStatus, ecgData };
};

const getPreviousNextBeatData = async (currentIndex, allBeatData, offsets, beatEpoch, ecgDataMap) => {
  let previousBeatData = null;
  let nextBeatData = null;

  if (currentIndex > 0) {
    previousBeatData = processBeatData(allBeatData[currentIndex - 1], beatEpoch, offsets, ecgDataMap);
  }

  if (currentIndex < allBeatData.length - 1) {
    nextBeatData = processBeatData(allBeatData[currentIndex + 1], beatEpoch, offsets, ecgDataMap);
  }

  return { previousBeatData, nextBeatData };
};

const handleBeatDataProcessing = async (currentBeatData, beatEpoch, ecgDataMap, i, allBeatData) => {
  let beatMarkers2s = [], hesBeatStatus2s = [], ecgData2s = [];
  let beatMarkers10s = [], hesBeatStatus10s = [], ecgData10s = [];

  const process2s = processBeatData(currentBeatData, beatEpoch, offset_2s, ecgDataMap)
    .then(data => {
      beatMarkers2s = data.beatMarkers;
      hesBeatStatus2s = data.hesBeatStatus;
      ecgData2s = data.ecgData;
    });

  const process10s = processBeatData(currentBeatData, beatEpoch, offset_10s, ecgDataMap)
    .then(data => {
      beatMarkers10s = data.beatMarkers;
      hesBeatStatus10s = data.hesBeatStatus;
      ecgData10s = data.ecgData;
    });

  const processPreviousNext2s = getPreviousNextBeatData(i, allBeatData, offset_2s, beatEpoch, ecgDataMap)
    .then(data => {
      if (data.previousBeatData) {
        beatMarkers2s = beatMarkers2s.concat(data.previousBeatData.beatMarkers);
        hesBeatStatus2s = hesBeatStatus2s.concat(data.previousBeatData.hesBeatStatus);
      }

      if (data.nextBeatData) {
        beatMarkers2s = beatMarkers2s.concat(data.nextBeatData.beatMarkers);
        hesBeatStatus2s = hesBeatStatus2s.concat(data.nextBeatData.hesBeatStatus);
      }
    });

  const processPreviousNext10s = getPreviousNextBeatData(i, allBeatData, offset_10s, beatEpoch, ecgDataMap)
    .then(data => {
      if (data.previousBeatData) {
        beatMarkers10s = beatMarkers10s.concat(data.previousBeatData.beatMarkers);
        hesBeatStatus10s = hesBeatStatus10s.concat(data.previousBeatData.hesBeatStatus);
      }

      if (data.nextBeatData) {
        beatMarkers10s = beatMarkers10s.concat(data.nextBeatData.beatMarkers);
        hesBeatStatus10s = hesBeatStatus10s.concat(data.nextBeatData.hesBeatStatus);
      }
    });

  return Promise.all([process2s, process10s, processPreviousNext2s, processPreviousNext10s])
    .then(() => ({
      beatEpoch,
      beat2s: {
        beatMarkers: beatMarkers2s,
        hesBeatStatus: hesBeatStatus2s,
        ecgData: ecgData2s,
      },
      beat10s: {
        beatMarkers: beatMarkers10s,
        hesBeatStatus: hesBeatStatus10s,
        ecgData: [],
      }
    }));
};

expose({
  createConfig: (initialData) => {
    const { samplingFrequency, ecgDataMap } = initialData;
    config.samplingFrequency = samplingFrequency;
    config.ecgDataMap = ecgDataMap;
  },
  downloadFirstBeatList: async (startTime, endTime, offset, beatEpoch) => {
    try {
      const fileIndex = getFileIndexByTimeFirstBeat(startTime, endTime);
      if (fileIndex === -1) {
        throw new Error('No file found for the given time range');
      }

      const allBeatData = await Promise.all(fileUrls.map((url, i) => getAdjacentFileBeatData(i)));
      const currentBeatData = await getAdjacentFileBeatData(fileIndex);
      const previousBeatData = fileIndex > 0 ? await getAdjacentFileBeatData(fileIndex - 1) : null;
      const nextBeatData = fileIndex < fileUrls.length - 1 ? await getAdjacentFileBeatData(fileIndex + 1) : null;

      let beatMarkers = [];
      let hesBeatStatus = [];
      let ecgData = [];

      const currentData = mergeBeatData(currentBeatData, beatEpoch, offset_2s);
      beatMarkers = beatMarkers.concat(currentData.beatMarkers);
      hesBeatStatus = hesBeatStatus.concat(currentData.hesBeatStatus);

      if (previousBeatData) {
        const previousData = mergeBeatData(previousBeatData, beatEpoch, offset_2s);
        beatMarkers = beatMarkers.concat(previousData.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(previousData.hesBeatStatus);
      }

      if (nextBeatData) {
        const nextData = mergeBeatData(nextBeatData, beatEpoch, offset_2s);
        beatMarkers = beatMarkers.concat(nextData.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(nextData.hesBeatStatus);
      }

      const currentData10s = mergeBeatData(currentBeatData, beatEpoch, offset_10s);
      beatMarkers = beatMarkers.concat(currentData10s.beatMarkers);
      hesBeatStatus = hesBeatStatus.concat(currentData10s.hesBeatStatus);

      if (previousBeatData) {
        const previousData10s = mergeBeatData(previousBeatData, beatEpoch, offset_10s);
        beatMarkers = beatMarkers.concat(previousData10s.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(previousData10s.hesBeatStatus);
      }

      if (nextBeatData) {
        const nextData10s = mergeBeatData(nextBeatData, beatEpoch, offset_10s);
        beatMarkers = beatMarkers.concat(nextData10s.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(nextData10s.hesBeatStatus);
      }

      ecgData = ecgData.concat(await handleDownloadData({
        ecgDataMap,
        beat: { id: beatEpoch },
        offset: offset_2s,
        dataLength: ecgDataMap.samplingFrequency * 2,
      }));

      const result = {
        beatEpochs: beatEpoch,
        beatMarkers,
        hesBeatStatus,
        ecgData,
      };
      postMessage({ result });
      return result;
    } catch (error) {
      console.error('Failed to download first beat: ', error);
      return null;
    } finally {
      self.close();
    }
  },
  downloadBeatListByDay: async () => {
    try {
      const ecgDataMap = config.ecgDataMap;
      if (!ecgDataMap) {
        throw new Error('ecgDataMap is not defined');
      }

      let allBeatData = await Promise.all(fileUrls.map((url, i) => getAdjacentFileBeatData(i)));

      for (let i = 0; i < allBeatData.length; i += 1) {
        const currentBeatData = allBeatData[i];
        await downloadECGDataByBeatTime(currentBeatData.start, ecgDataMap);

        const { start, stop } = studySummariesJson[i];
        const startDate = new Date(start);
        const stopDate = new Date(stop);
        const date = startDate.toISOString().split('T')[0];
        const hour = startDate.getHours();

        currentBeatData.beatEpochs.forEach(async beatEpoch => {
          const hourData = await handleBeatDataProcessing(currentBeatData, beatEpoch, ecgDataMap, i, allBeatData);
          hourData.date = date;
          hourData.hour = hour;
          self.postMessage({ hourData });
        });
      }

      allBeatData = null;
      CACHE_ECG.clear();
      CACHE_BEAT.clear();
      return 'done';
    } catch (error) {
      console.error('Failed to download beat data by day: ', error);
      return [];
    } finally {
      self.close();
    }
  },
});
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
import axios from 'axios';
import { expose } from 'threads/worker';
import _, { chunk } from 'lodash';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
import TTLCache from '@isaacs/ttlcache';
import studySummariesJson from '../../../../dummyData/studySummaries.json';

dayjs.extend(isBetween);

const fileUrls = [
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-08-08-14%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-09-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-10-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-11-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-12-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-13-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-14-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-15-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-16-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-17-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-18-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-19-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-20-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-21-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-22-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-23-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-00-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-01-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-02-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-03-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-04-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-05-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-06-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-07-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-08-00-00%2B28-final.beat',
];

const CACHE_ECG = new TTLCache({ ttl: 180000 }); // Cache ECG files for 3 minutes
const CACHE_BEAT = new TTLCache({ ttl: 180000 }); // Cache BEAT files for 3 minutes
const config = {};

function extractFilename(url) {
  // Sử dụng URL API để lấy pathname từ URL
  const urlObj = new URL(url);
  const { pathname } = urlObj;

  // Sử dụng Regular Expression để lấy tên tệp từ pathname
  const regex = /[^/]+$/; // Biểu thức chính quy để tìm phần cuối cùng sau dấu "/"
  const match = pathname.match(regex);

  if (match) {
    return match[0];
  }
  return null;
}

const downloadFile = async (url, cache) => {
  const filename = extractFilename(url);
  if (cache.has(filename)) {
    return cache.get(filename);
  }
  try {
    const response = await axios({
      method: 'GET',
      url,
      responseType: 'arraybuffer',
    });

    cache.set(filename, response.data);
    return response.data;
  } catch (error) {
    console.error('Failed to download file: ', error);
    return null;
  }
};

const getECGDataForHour = async (hourIndex, ecgDataMap) => {
  if (hourIndex < 0 || hourIndex >= ecgDataMap.data.length) {
    return null;
  }
  const ecgDataEntry = ecgDataMap.data[hourIndex];
  const urlECG = `${ecgDataEntry.dataUrl}?${ecgDataMap.querySignature}`;
  const fileData = await downloadFile(urlECG, CACHE_ECG);
  if (!fileData) return null;

  return {
    start: ecgDataEntry.start,
    stop: ecgDataEntry.stop,
    data: fileData,
  };
};
const getFileIndexByTimeFirstBeat = (startTime, endTime) => {
  for (let i = 0; i < studySummariesJson.length; i += 1) {
    const study = studySummariesJson[i];
    const studyStartTime = dayjs(study.start);
    const studyEndTime = dayjs(study.stop);
    if (dayjs(startTime).isBetween(studyStartTime, studyEndTime, null, '[]')
        || dayjs(endTime).isBetween(studyStartTime, studyEndTime, null, '[]')) {
      return i;
    }
  }
  return -1; // Not found
};


const getFileIndexByTime = (time, ecgDataMap) => {
  for (let i = 0; i < ecgDataMap.data.length; i += 1) {
    const entry = ecgDataMap.data[i];
    const startTime = dayjs(entry.start.$d);
    const stopTime = dayjs(entry.stop.$d);
    if (dayjs(time).isBetween(startTime, stopTime, null, '[]')) {
      return i;
    }
  }
  return -1;
};

const manageECGCache = async (currentHourIndex, ecgDataMap) => {
  const hoursToDownload = [currentHourIndex - 1, currentHourIndex, currentHourIndex + 1];
  const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

  // Download the required ECG files
  await Promise.all(validHours.map(hour => getECGDataForHour(hour, ecgDataMap)));

  if (CACHE_ECG.size > 3) {
    const keysToDelete = Array.from(CACHE_ECG.keys())
      .filter(key => !validHours.includes(parseInt(key.split('data-hourly-')[1].split('-')[0], 10)))
      .sort();

    // Delete entries until cache size is 3 or less
    keysToDelete.slice(0, CACHE_ECG.size - 3).forEach(key => CACHE_ECG.delete(key));
  }
};


const downloadECGDataByBeatTime = async (beatTime, ecgDataMap) => {
  try {
    const beatTimestamp = dayjs(beatTime).valueOf();
    const fileIndex = getFileIndexByTime(beatTimestamp, ecgDataMap);

    if (fileIndex === -1) {
      throw new Error('No file found for the given beat time');
    }

    await manageECGCache(fileIndex, ecgDataMap);

    const hoursToDownload = [fileIndex - 1, fileIndex, fileIndex + 1];
    const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

    const ecgDataPromises = validHours.map(hour => getECGDataForHour(hour, ecgDataMap));
    const ecgDataResults = await Promise.all(ecgDataPromises);

    const ecgDataList = ecgDataResults.filter(ecgData => ecgData !== null);

    return ecgDataList;
  } catch (error) {
    console.error('Failed to download ECG data by beat time: ', error);
    return [];
  }
};


const mergeBeatData = (beatData, beatEpoch, offset) => {
  const beatMarkers = [];
  const hesBeatStatus = [];

  if (beatData) {
    beatData.beatEpochs.forEach((epoch, index) => {
      if (epoch >= (beatEpoch - offset) && epoch <= (beatEpoch + offset)) {
        const sample = Math.round(
          ((epoch - (beatEpoch - offset)) * config.samplingFrequency) / 1000,
        );
        beatMarkers.push(sample);
        hesBeatStatus.push(beatData.hesBeatStatus[index]);
      }
    });
  }

  return { beatMarkers, hesBeatStatus };
};

const downloadSelectedEcgData = async (ranges, querySignature) => {
  try {
    if (ranges.length) {
      const blobPromises = ranges.map(async (range) => {
        const cacheKey = `${range.dataUrl}?${querySignature}`;
        const filename = extractFilename(cacheKey);
        if (CACHE_ECG.has(filename)) {
          const cachedData = CACHE_ECG.get(filename);
          if (range.dataRange.start >= 0 && range.dataRange.stop <= cachedData.byteLength) {
            return cachedData.slice(range.dataRange.start, range.dataRange.stop + 1);
          }
          console.error('Invalid range:', range.dataRange);
          return null;
        }
        console.error('No cached data for URL:', range.dataUrl);
        return null;
      });

      const blob = (await Promise.all(blobPromises)).filter(data => data !== null);
      let concatenated = await new Blob(blob).arrayBuffer();
      const { byteLength } = concatenated;
      if (byteLength % 2 !== 0) {
        concatenated = concatenated.slice(0, byteLength - 1);
      }

      return new Int16Array(concatenated);
    }
    return [];
  } catch (error) {
    console.error('Failed to download selected ECG data: ', error);
    return [];
  }
};


const convertBinaryToECGData = (totalChannel, binaryData) => {
  if (!binaryData) {
    return undefined;
  }
  const ecgData = Array.from({ length: totalChannel }, () => []);
  for (let i = 0; i < binaryData.length; i += totalChannel) {
    for (let j = 0; j < totalChannel; j++) {
      ecgData[j].push(binaryData[i + j]);
    }
  }
  return ecgData;
};

const getSineWaveDataArray = (diffMilliseconds, samplingFrequency, adcGain) => {
  const halfSamplingFrequency = samplingFrequency / 2;
  const sineWaveCount = diffMilliseconds / 1000;

  const sineWaveDataArray = _.map(_.range(samplingFrequency), (x, i) => {
    const sample = Math.sin(x * ((2 * Math.PI) / samplingFrequency)) * (0.75 * (adcGain / 2));
    return i <= halfSamplingFrequency ? Math.round(sample - (1.25 * adcGain)) : Math.round(sample + (1.25 * adcGain));
  });
  let resultSineWave = [];
  const wholeSineWaveCount = Math.floor(sineWaveCount);
  const fractionalSineWaveCount = sineWaveCount - wholeSineWaveCount;
  for (let k = 0; k < wholeSineWaveCount; k += 1) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray);
  }
  if (fractionalSineWaveCount) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray.slice(0, Math.round(fractionalSineWaveCount * sineWaveDataArray.length)));
  }
  return resultSineWave;
};

const combineRanges = (ranges, start, stop) => {
  const adjustedRanges = ranges
    .map((subarray) => {
      let [subStart, subStop] = subarray;
      subStart = Math.max(subStart, start);
      subStop = Math.min(subStop, stop);
      return subStart <= subStop ? [subStart, subStop] : null;
    })
    .filter(subarray => subarray !== null);

  if (start <= stop) {
    adjustedRanges.unshift([start, start]);
    adjustedRanges.push([stop, stop]);
  }

  return adjustedRanges;
};


const addSineWaveToEcgData = (selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength = 75000) => {
  if (selectedEcgData[0].length === dataLength) {
    return selectedEcgData;
  }
  const { samplingFrequency, gain } = ecgDataMap;
  const ecgData = _.map(selectedEcgData, () => []);
  const rangesEpochs = _.map(ranges, x => [dayjs(x.start.$d).valueOf(), dayjs(x.stop.$d).valueOf()]);
  const startEpoch = dayjs(startMoment).valueOf();
  const stopEpoch = dayjs(stopMoment).valueOf();
  const rangesData = combineRanges(rangesEpochs, startEpoch, stopEpoch);
  let dataIndex = 0;
  _.forEach(rangesData, (range, i) => {
    if (i > 0) {
      const currStart = range[0];
      const currStop = range[1];
      const prevStop = rangesData[i - 1][1];
      if (currStart - prevStop > 0) {
        const sineWave = getSineWaveDataArray(currStart - prevStop, samplingFrequency, gain);
        _.forEach(ecgData, (data) => {
          data.push(...sineWave);
        });
      }
      if (currStop - currStart > 0) {
        const diffMs = (currStop - currStart) / 1000;
        const dataLength = Math.round(diffMs * samplingFrequency);
        _.forEach(ecgData, (data, j) => {
          data.push(...selectedEcgData[j].slice(dataIndex, dataIndex + dataLength));
        });
        dataIndex += dataLength;
      }
    }
  });
  return ecgData;
};

const getSizeData = (stop, start, bytesPerSecond) => {
  const diffTime = +stop - +start;
  return diffTime * bytesPerSecond;
};

const addDataRanges = ({
  summaries, start: startEvent, stop: stopEvent, samplingFrequency, channels,
}) => {
  const bytesPerSample = 2;
  const samplePerMsSecond = channels.length * bytesPerSample * samplingFrequency / 1000;
  const lenHourlySummaries = summaries.length;
  for (let i = 0; i < lenHourlySummaries; i += 1) {
    const hourlySummary = summaries[i];
    const { start: startHs, stop: stopHs } = hourlySummary;
    const _startHs = dayjs(startHs.$d);
    const _stopHs = dayjs(stopHs.$d);
    const sizeData = getSizeData(
      Math.min(_stopHs, stopEvent),
      Math.max(_startHs, startEvent),
      samplePerMsSecond,
    );
    const readPosition = i === 0
      ? getSizeData(startEvent, Math.min(_startHs, startEvent), samplePerMsSecond)
      : 0;
    hourlySummary.dataRange = {
      start: Math.ceil(readPosition),
      stop: Math.floor(readPosition + sizeData),
    };
  }
};

const filterHourlySummaryRanges = ({
  summaries, start, stop, samplingFrequency, channels, willAddDataRanges,
}) => {
  const ranges = summaries.data.filter(summary => _.every([
    +dayjs(summary.stop.$d) > +start,
    +dayjs(summary.start.$d) < +stop,
  ]));
  if (willAddDataRanges) {
    addDataRanges({
      summaries: ranges,
      start,
      stop,
      samplingFrequency,
      channels,
    });
  }
  return ranges;
};

const handleDownloadData = async ({
  ecgDataMap, beat, offset, momentObject = {}, dataLength, config,
}) => {
  const { customOffset } = config || {};
  let epoch = beat?.start ? new Date(beat.start).getTime() : beat?.id;
  const dividedNumber = 1000 / ecgDataMap.samplingFrequency;
  epoch = Math.floor(epoch / dividedNumber) * dividedNumber;
  let startMoment;
  let stopMoment;
  if (!_.isEmpty(momentObject)) {
    startMoment = momentObject.startMoment;
    stopMoment = momentObject.stopMoment;
  } else if (!_.isEmpty(customOffset)) {
    startMoment = dayjs(epoch - customOffset[0]);
    stopMoment = dayjs(epoch + customOffset[1]);
  } else if (offset) {
    startMoment = dayjs(epoch - offset);
    stopMoment = dayjs(epoch + offset);
  }
  const { channels, samplingFrequency } = ecgDataMap;
  const ranges = filterHourlySummaryRanges({
    summaries: ecgDataMap,
    start: startMoment,
    stop: stopMoment,
    channels,
    samplingFrequency,
    willAddDataRanges: true,
  });

  try {
    const downloadedSelectedEcgData = await downloadSelectedEcgData(ranges, ecgDataMap.querySignature);
    if (downloadedSelectedEcgData?.length) {
      const selectedEcgData = convertBinaryToECGData(channels.length, downloadedSelectedEcgData);
      const addedSineWaveSelectedEcgData = addSineWaveToEcgData(selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength);
      return addedSineWaveSelectedEcgData;
    }
    throw new Error('Error');
  } catch (error) {
    throw error;
  }
};

const processChunkedArray = (chunkedArray, start, fileIndex) => {
  const beatData = {
    start,
    stop: studySummariesJson[fileIndex].stop,
    channel: null,
    beatEpochs: [],
    hesBeatStatus: [],
  };

  chunkedArray.forEach((item) => {
    const channel = item[0];
    const hesBeatStatus = item.slice(1, 2);
    const beatBytes = item.slice(2, 5).map(byte => (byte < 0 ? 256 + byte : byte));

    const beatPosition = (beatBytes[0] << 16) + (beatBytes[1] << 8) + beatBytes[2];
    const beatEpoch = dayjs(start).add(beatPosition / config.samplingFrequency, 'second').valueOf();
    beatData.channel = channel;
    beatData.beatEpochs.push(beatEpoch);
    beatData.hesBeatStatus.push(hesBeatStatus);
  });
  // Giải phóng bộ nhớ sau khi xử lý
  chunkedArray.length = 0;

  return beatData;
};

const getAdjacentFileBeatData = async (fileIndex) => {
  if (fileIndex < 0 || fileIndex >= fileUrls.length) {
    return null;
  }
  const fileUrl = fileUrls[fileIndex];
  const fileData = await downloadFile(fileUrl, CACHE_BEAT);
  if (!fileData) return null;

  const array = new Int8Array(fileData);
  const chunkedArray = chunk(array, 5);
  const { start } = studySummariesJson[fileIndex];
  const beatData = processChunkedArray(chunkedArray, start, fileIndex);
  return beatData;
};

expose({
  createConfig: (initialData) => {
    const { samplingFrequency } = initialData;
    config.samplingFrequency = samplingFrequency;
  },
  downloadFirstBeatList: async (startTime, endTime, offset, ecgDataMap, beatEpoch) => {
    try {
      const fileIndex = getFileIndexByTimeFirstBeat(startTime, endTime);
      if (fileIndex === -1) {
        throw new Error('No file found for the given time range');
      }

      const currentBeatData = await getAdjacentFileBeatData(fileIndex);
      const previousBeatData = fileIndex > 0 ? await getAdjacentFileBeatData(fileIndex - 1) : null;
      const nextBeatData = fileIndex < fileUrls.length - 1 ? await getAdjacentFileBeatData(fileIndex + 1) : null;
      let beatMarkers = [];
      let hesBeatStatus = [];


      // Merge data for the current beatEpoch
      const currentData = mergeBeatData(currentBeatData, beatEpoch, offset);
      beatMarkers = beatMarkers.concat(currentData.beatMarkers);
      hesBeatStatus = hesBeatStatus.concat(currentData.hesBeatStatus);

      // Merge data from previous file if available
      if (previousBeatData) {
        const previousData = mergeBeatData(previousBeatData, beatEpoch, offset);
        beatMarkers = beatMarkers.concat(previousData.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(previousData.hesBeatStatus);
      }

      // Merge data from next file if available
      if (nextBeatData) {
        const nextData = mergeBeatData(nextBeatData, beatEpoch, offset);
        beatMarkers = beatMarkers.concat(nextData.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(nextData.hesBeatStatus);
      }

      const result = {
        beatEpochs: beatEpoch,
        beatMarkers,
        hesBeatStatus,
      };
      postMessage({ result });
      return result;
    } catch (error) {
      console.error('Failed to download first beat: ', error);
      return null;
    } finally {
      self.close();
    }
  },
  downloadBeatListByDay: async (ecgDataMap) => {
    const offset_2s = 2000;
    const offset_10s = 10000;
    try {
      let dailyBeatData = [];

      let allBeatData = await Promise.all(fileUrls.map((url, i) => getAdjacentFileBeatData(i)));

      for (let i = 0; i < allBeatData.length; i += 1) {
        const currentBeatData = allBeatData[i];
        let previousBeatData_2s = null;
        let nextBeatData_2s = null;

        let previousBeatData_10s = null;
        let nextBeatData_10s = null;

        const { start, stop } = studySummariesJson[i];
        const startDate = new Date(start);
        const stopDate = new Date(stop);

        const isNearStart_2s = epoch => epoch <= (startDate.getTime() + offset_2s);
        const isNearEnd_2s = epoch => epoch >= (stopDate.getTime() - offset_2s);

        if (isNearStart_2s(currentBeatData.beatEpochs[0])) {
          previousBeatData_2s = i > 0 ? allBeatData[i - 1] : null;
        }

        if (isNearEnd_2s(currentBeatData.beatEpochs[currentBeatData.beatEpochs.length - 1])) {
          nextBeatData_2s = i < allBeatData.length - 1 ? allBeatData[i + 1] : null;
        }

        const isNearStart_10s = epoch => epoch <= (startDate.getTime() + offset_10s);
        const isNearEnd_10s = epoch => epoch >= (stopDate.getTime() - offset_10s);

        if (isNearStart_10s(currentBeatData.beatEpochs[0])) {
          previousBeatData_10s = i > 0 ? allBeatData[i - 1] : null;
        }

        if (isNearEnd_10s(currentBeatData.beatEpochs[currentBeatData.beatEpochs.length - 1])) {
          nextBeatData_10s = i < allBeatData.length - 1 ? allBeatData[i + 1] : null;
        }

        let dateEntry = dailyBeatData.find(entry => entry.date === startDate.toISOString().split('T')[0]);
        if (!dateEntry) {
          dateEntry = { date: startDate.toISOString().split('T')[0], hours: [] };
          dailyBeatData.push(dateEntry);
        }

        let hourEntry = dateEntry.hours.find(entry => entry.hour === startDate.getHours());
        if (!hourEntry) {
          hourEntry = { hour: startDate.getHours(), value: [] };
          dateEntry.hours.push(hourEntry);
        }

        for (const beatEpoch of currentBeatData.beatEpochs) {
          let beatMarkers_2s = [];
          let hesBeatStatus_2s = [];
          let beatMarkers_10s = [];
          let hesBeatStatus_10s = [];
          let ecgData_2s = [];
          let ecgData_10s = [];
          // handle 2s data

          const currentData_2s = mergeBeatData(currentBeatData, beatEpoch, offset_2s);
          beatMarkers_2s = beatMarkers_2s.concat(currentData_2s.beatMarkers);
          hesBeatStatus_2s = hesBeatStatus_2s.concat(currentData_2s.hesBeatStatus);
          await downloadECGDataByBeatTime(beatEpoch, ecgDataMap);
          ecgData_2s = ecgData_2s.concat(await handleDownloadData({
            ecgDataMap,
            beat: { id: beatEpoch },
            offset_2s,
            dataLength: ecgDataMap.samplingFrequency * 2,
          }));

          if (previousBeatData_2s && isNearStart_2s(beatEpoch)) {
            const previousData_2s = mergeBeatData(previousBeatData_2s, beatEpoch, offset_2s);
            beatMarkers_2s = beatMarkers_2s.concat(previousData_2s.beatMarkers);
            hesBeatStatus_2s = hesBeatStatus_2s.concat(previousData_2s.hesBeatStatus);
          }

          if (nextBeatData_2s && isNearEnd_2s(beatEpoch)) {
            const nextData_2s = mergeBeatData(nextBeatData_2s, beatEpoch, offset_2s);
            beatMarkers_2s = beatMarkers_2s.concat(nextData_2s.beatMarkers);
            hesBeatStatus_2s = hesBeatStatus_2s.concat(nextData_2s.hesBeatStatus);
          }

          // handle 10s data
          const currentData_10s = mergeBeatData(currentBeatData, beatEpoch, offset_10s);
          beatMarkers_10s = beatMarkers_10s.concat(currentData_10s.beatMarkers);
          hesBeatStatus_10s = hesBeatStatus_10s.concat(currentData_10s.hesBeatStatus);
          await downloadECGDataByBeatTime(beatEpoch, ecgDataMap);
          ecgData_10s = ecgData_10s.concat(await handleDownloadData({
            ecgDataMap,
            beat: { id: beatEpoch },
            offset_10s,
            dataLength: ecgDataMap.samplingFrequency * 2,
          }));

          if (previousBeatData_10s && isNearStart_10s(beatEpoch)) {
            const previousData_10s = mergeBeatData(previousBeatData_10s, beatEpoch, offset_10s);
            beatMarkers_10s = beatMarkers_10s.concat(previousData_10s.beatMarkers);
            hesBeatStatus_10s = hesBeatStatus_10s.concat(previousData_10s.hesBeatStatus);
          }

          if (nextBeatData_10s && isNearEnd_10s(beatEpoch)) {
            const nextData_10s = mergeBeatData(nextBeatData_10s, beatEpoch, offset_10s);
            beatMarkers_10s = beatMarkers_10s.concat(nextData_10s.beatMarkers);
            hesBeatStatus_10s = hesBeatStatus_10s.concat(nextData_10s.hesBeatStatus);
          }

          hourEntry.value.push({
            beatEpochs: beatEpoch,
            beat2s : {   
              beatMarkers_2s,
              hesBeatStatus_2s,
              ecgData_2s,
            },
            beat10s : {   
              beatMarkers_10s,
              hesBeatStatus_10s,
              ecgData_10s
            },
            ecgData,
          });
        }

        const { date } = dateEntry;
        self.postMessage({ date, hourEntry });
        // Free memory
        hourEntry = null;
        dateEntry = null;
        }
      dailyBeatData = null;
      allBeatData = null;
      CACHE_ECG.clear();
      CACHE_BEAT.clear();
      return 'done';
    } catch (error) {
      console.error('Failed to download beat data by day: ', error);
      return [];
    } finally {
      self.close();
    }
  },
});
let rawPrize = prompt("Enter your first name");

​
let b = 0;      // b contains 0

b += 1;         // b contains 1

b++;            // b contains 2

console.log(b); // Shows 2

​
const nb = Number(prompt("Enter a number:")); // nb's type is number
import axios from 'axios';
import { expose } from 'threads/worker';
import _, { chunk } from 'lodash';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
import TTLCache from '@isaacs/ttlcache';
import studySummariesJson from '../../../../dummyData/studySummaries.json';

dayjs.extend(isBetween);

const fileUrls = [
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-08-08-14%2B28-final.beat',
  // Add other URLs here...
];

const CACHE_ECG = new TTLCache({ ttl: 3600000 }); // Cache ECG files for 1 hour
const CACHE_BEAT = new TTLCache({ ttl: 3600000 }); // Cache BEAT files for 1 hour
const config = {};

const downloadFile = async (url, cache) => {
  if (cache.has(url)) {
    return cache.get(url);
  }

  try {
    const response = await axios({
      method: 'GET',
      url,
      responseType: 'arraybuffer',
    });

    cache.set(url, response.data);
    return response.data;
  } catch (error) {
    console.error('Failed to download file: ', error);
    return null;
  }
};

const getECGDataForHour = async (hourIndex, ecgDataMap) => {
  if (hourIndex < 0 || hourIndex >= ecgDataMap.data.length) {
    return null;
  }
  const ecgDataEntry = ecgDataMap.data[hourIndex];
  const urlECG = `${ecgDataEntry.dataUrl}?${ecgDataMap.querySignature}`;
  const fileData = await downloadFile(urlECG, CACHE_ECG);
  if (!fileData) return null;

  return {
    start: ecgDataEntry.start,
    stop: ecgDataEntry.stop,
    data: fileData,
  };
};

const manageECGCache = async (currentHourIndex, ecgDataMap) => {
  const hoursToDownload = [currentHourIndex - 1, currentHourIndex, currentHourIndex + 1];
  const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

  // Download necessary ECG files and delete unnecessary files from the cache
  for (const hour of validHours) {
    await getECGDataForHour(hour, ecgDataMap);
  }

  const keysToDelete = [];
  for (const [key, value] of CACHE_ECG.entries()) {
    const urlHour = parseInt(key.split('data-hourly-')[1].split('-')[0]);
    if (!validHours.includes(urlHour)) {
      keysToDelete.push(key);
    }
  }

  for (const key of keysToDelete) {
    CACHE_ECG.delete(key);
  }
};

const downloadECGDataByBeatTime = async (beatTime, ecgDataMap) => {
  try {
    const beatTimestamp = dayjs(beatTime).valueOf();
    const fileIndex = getFileIndexByTime(beatTimestamp, ecgDataMap);

    if (fileIndex === -1) {
      throw new Error('No file found for the given beat time');
    }

    await manageECGCache(fileIndex, ecgDataMap);

    const ecgDataList = [];
    const hoursToDownload = [fileIndex - 1, fileIndex, fileIndex + 1];
    const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

    for (const hour of validHours) {
      const ecgData = await getECGDataForHour(hour, ecgDataMap);
      if (ecgData) {
        ecgDataList.push(ecgData);
      }
    }

    return ecgDataList;
  } catch (error) {
    console.error('Failed to download ECG data by beat time: ', error);
    return [];
  }
};

const mergeBeatData = (beatData, beatEpoch, offset) => {
  const beatMarkers = [];
  const hesBeatStatus = [];

  if (beatData) {
    beatData.beatEpochs.forEach((epoch, index) => {
      if (epoch >= (beatEpoch - offset) && epoch <= (beatEpoch + offset)) {
        const sample = Math.round(
          ((epoch - (beatEpoch - offset)) * config.samplingFrequency) / 1000,
        );
        beatMarkers.push(sample);
        hesBeatStatus.push(beatData.hesBeatStatus[index]);
      }
    });
  }

  return { beatMarkers, hesBeatStatus };
};

const downloadSelectedEcgData = async (ranges, querySignature) => {
  try {
    if (ranges.length) {
      const blob = [];
      for (const range of ranges) {
        const cacheKey = `${range.dataUrl}?${querySignature}`;
        if (CACHE_ECG.has(cacheKey)) {
          const cachedData = CACHE_ECG.get(cacheKey);
          if (range.dataRange.start >= 0 && range.dataRange.stop <= cachedData.byteLength) {
            const slicedData = cachedData.slice(range.dataRange.start, range.dataRange.stop + 1);
            blob.push(slicedData);
          } else {
            console.error('Invalid range:', range.dataRange);
          }
        } else {
          console.error('No cached data for URL:', range.dataUrl);
        }
      }
      let concatenated = await new Blob(blob).arrayBuffer();
      const { byteLength } = concatenated;
      if (byteLength % 2 !== 0) {
        concatenated = concatenated.slice(0, byteLength - 1);
      }

      return new Int16Array(concatenated);
    }
    return [];
  } catch (error) {
    console.error('Failed to download selected ECG data: ', error);
    return [];
  }
};

const convertBinaryToECGData = (totalChannel, binaryData) => {
  if (!binaryData) {
    return undefined;
  }
  const ecgData = Array.from({ length: totalChannel }, () => []);
  for (let i = 0; i < binaryData.length; i += totalChannel) {
    for (let j = 0; j < totalChannel; j++) {
      ecgData[j].push(binaryData[i + j]);
    }
  }
  return ecgData;
};

const addSineWaveToEcgData = (selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength = 75000) => {
  if (selectedEcgData[0].length === dataLength) {
    return selectedEcgData;
  }
  const { samplingFrequency, gain } = ecgDataMap;
  const ecgData = _.map(selectedEcgData, () => []);
  const rangesEpochs = _.map(ranges, x => [dayjs(x.start.$d).valueOf(), dayjs(x.stop.$d).valueOf()]);
  const startEpoch = dayjs(startMoment).valueOf();
  const stopEpoch = dayjs(stopMoment).valueOf();
  const rangesData = combineRanges(rangesEpochs, startEpoch, stopEpoch);
  let dataIndex = 0;
  _.forEach(rangesData, (range, i) => {
    if (i > 0) {
      const currStart = range[0];
      const currStop = range[1];
      const prevStop = rangesData[i - 1][1];
      if (currStart - prevStop > 0) {
        const sineWave = getSineWaveDataArray(currStart - prevStop, samplingFrequency, gain);
        _.forEach(ecgData, (data) => {
          data.push(...sineWave);
        });
      }
      if (currStop - currStart > 0) {
        const diffMs = (currStop - currStart) / 1000;
        const dataLength = Math.round(diffMs * samplingFrequency);
        _.forEach(ecgData, (data, j) => {
          data.push(...selectedEcgData[j].slice(dataIndex, dataIndex + dataLength));
        });
        dataIndex += dataLength;
      }
    }
  });
  return ecgData;
};

const getSineWaveDataArray = (diffMilliseconds, samplingFrequency, adcGain) => {
  const halfSamplingFrequency = samplingFrequency / 2;
  const sineWaveCount = diffMilliseconds / 1000;

  const sineWaveDataArray = _.map(_.range(samplingFrequency), (x, i) => {
    const sample = Math.sin(x * ((2 * Math.PI) / samplingFrequency)) * (0.75 * (adcGain / 2));
    return i <= halfSamplingFrequency ? Math.round(sample - (1.25 * adcGain)) : Math.round(sample + (1.25 * adcGain));
  });
  let resultSineWave = [];
  const wholeSineWaveCount = Math.floor(sineWaveCount);
  const fractionalSineWaveCount = sineWaveCount - wholeSineWaveCount;
  for (let k = 0; k < wholeSineWaveCount; k += 1) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray);
  }
  if (fractionalSineWaveCount) {
    resultSineWave = resultSineWave.concat(sineWaveDataArray.slice(0, Math.round(fractionalSineWaveCount * sineWaveDataArray.length)));
  }
  return resultSineWave;
};

const combineRanges = (ranges, start, stop) => {
  const result = [];
  for (const subarray of ranges) {
    let [subStart, subStop] = subarray;
    subStart = Math.max(subStart, start);
    subStop = Math.min(subStop, stop);
    if (subStart <= subStop) {
      result.push([subStart, subStop]);
    }
  }
  if (start <= stop) {
    result.unshift([start, start]);
    result.push([stop, stop]);
  }
  return result;
};

const handleDownloadData = async ({ ecgDataMap, beat, offset, momentObject = {}, dataLength, config }) => {
  const { customOffset } = config || {};
  let epoch = beat?.start ? new Date(beat.start).getTime() : beat?.id;
  const dividedNumber = 1000 / ecgDataMap.samplingFrequency;
  epoch = Math.floor(epoch / dividedNumber) * dividedNumber;
  let startMoment;
  let stopMoment;
  if (!_.isEmpty(momentObject)) {
    startMoment = momentObject.startMoment;
    stopMoment = momentObject.stopMoment;
  } else if (!_.isEmpty(customOffset)) {
    startMoment = dayjs(epoch - customOffset[0]);
    stopMoment = dayjs(epoch + customOffset[1]);
  } else if (offset) {
    startMoment = dayjs(epoch - offset);
    stopMoment = dayjs(epoch + offset);
  }
  const { channels, samplingFrequency } = ecgDataMap;
  const ranges = filterHourlySummaryRanges({
    summaries: ecgDataMap,
    start: startMoment,
    stop: stopMoment,
    channels,
    samplingFrequency,
    willAddDataRanges: true,
  });

  try {
    let downloadedSelectedEcgData = await downloadSelectedEcgData(ranges, ecgDataMap.querySignature);
    if (downloadedSelectedEcgData?.length) {
      const selectedEcgData = convertBinaryToECGData(channels.length, downloadedSelectedEcgData);
      const addedSineWaveSelectedEcgData = addSineWaveToEcgData(selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength);
      return addedSineWaveSelectedEcgData;
    }
    throw new Error('Error');
  } catch (error) {
    throw error;
  }
};

const filterHourlySummaryRanges = ({
  summaries, start, stop, samplingFrequency, channels, willAddDataRanges,
}) => {
  const ranges = summaries.data.filter(summary => _.every([
    +dayjs(summary.stop.$d) > +start,
    +dayjs(summary.start.$d) < +stop,
  ]));
  if (willAddDataRanges) {
    addDataRanges({
      summaries: ranges,
      start,
      stop,
      samplingFrequency,
      channels,
    });
  }
  return ranges;
};

const addDataRanges = ({
  summaries, start: startEvent, stop: stopEvent, samplingFrequency, channels,
}) => {
  const bytesPerSample = 2;
  const samplePerMsSecond = channels.length * bytesPerSample * samplingFrequency / 1000;
  const lenHourlySummaries = summaries.length;
  for (let i = 0; i < lenHourlySummaries; i += 1) {
    const hourlySummary = summaries[i];
    const { start: startHs, stop: stopHs } = hourlySummary;
    const _startHs = dayjs(startHs.$d);
    const _stopHs = dayjs(stopHs.$d);
    const sizeData = getSizeData(
      Math.min(_stopHs, stopEvent),
      Math.max(_startHs, startEvent),
      samplePerMsSecond,
    );
    const readPosition = i === 0
      ? getSizeData(startEvent, Math.min(_startHs, startEvent), samplePerMsSecond)
      : 0;
    hourlySummary.dataRange = {
      start: Math.ceil(readPosition),
      stop: Math.floor(readPosition + sizeData),
    };
  }
};

const getSizeData = (stop, start, bytesPerSecond) => {
  const diffTime = +stop - +start;
  return diffTime * bytesPerSecond;
};

const processChunkedArray = (chunkedArray, start, fileIndex) => {
  const beatData = {
    start,
    stop: studySummariesJson[fileIndex].stop,
    channel: null,
    beatEpochs: [],
    hesBeatStatus: [],
  };

  chunkedArray.forEach((item) => {
    const channel = item[0];
    const hesBeatStatus = item.slice(1, 2);
    const beatBytes = item.slice(2, 5).map(byte => (byte < 0 ? 256 + byte : byte));

    const beatPosition = (beatBytes[0] << 16) + (beatBytes[1] << 8) + beatBytes[2];
    const beatEpoch = dayjs(start).add(beatPosition / config.samplingFrequency, 'second').valueOf();
    beatData.channel = channel;
    beatData.beatEpochs.push(beatEpoch);
    beatData.hesBeatStatus.push(hesBeatStatus);
  });

  return beatData;
};

const getAdjacentFileBeatData = async (fileIndex) => {
  if (fileIndex < 0 || fileIndex >= fileUrls.length) {
    return null;
  }
  const fileUrl = fileUrls[fileIndex];
  const fileData = await downloadFile(fileUrl, CACHE_BEAT);
  if (!fileData) return null;

  const array = new Int8Array(fileData);
  const chunkedArray = chunk(array, 5);

  const { start } = studySummariesJson[fileIndex];
  const beatData = processChunkedArray(chunkedArray, start, fileIndex);

  return beatData;
};

expose({
  createConfig: (initialData) => {
    const { samplingFrequency } = initialData;
    config.samplingFrequency = samplingFrequency;
  },
  downloadFirstBeatList: async (startTime, endTime, offset) => {
    try {
      const fileIndex = getFileIndexByTimeFirstBeat(startTime, endTime, studySummariesJson);
      if (fileIndex === -1) {
        throw new Error('No file found for the given time range');
      }

      const currentBeatData = await getAdjacentFileBeatData(fileIndex);
      let previousBeatData = null;
      let nextBeatData = null;

      if (fileIndex > 0) {
        previousBeatData = await getAdjacentFileBeatData(fileIndex - 1);
      }
      if (fileIndex < studySummariesJson.length - 1) {
        nextBeatData = await getAdjacentFileBeatData(fileIndex + 1);
      }

      const { start } = studySummariesJson[fileIndex];
      const startDate = new Date(start);

      const hourEntry = { hour: startDate.getHours(), value: [] };

      currentBeatData.beatEpochs.forEach((beatEpoch) => {
        let beatMarkers = [];
        let hesBeatStatus = [];

        const currentData = mergeBeatData(currentBeatData, beatEpoch, offset);
        beatMarkers = beatMarkers.concat(currentData.beatMarkers);
        hesBeatStatus = hesBeatStatus.concat(currentData.hesBeatStatus);

        if (previousBeatData) {
          const previousData = mergeBeatData(previousBeatData, beatEpoch, offset);
          beatMarkers = beatMarkers.concat(previousData.beatMarkers);
          hesBeatStatus = hesBeatStatus.concat(previousData.hesBeatStatus);
        }

        if (nextBeatData) {
          const nextData = mergeBeatData(nextBeatData, beatEpoch, offset);
          beatMarkers = beatMarkers.concat(nextData.beatMarkers);
          hesBeatStatus = hesBeatStatus.concat(nextData.hesBeatStatus);
        }

        hourEntry.value.push({
          beatEpochs: beatEpoch,
          beatMarkers,
          hesBeatStatus,
        });

        // Free memory
        beatMarkers = null;
        hesBeatStatus = null;
      });

      const result = hourEntry.value;
      self.postMessage({ result });

      // Free memory
      currentBeatData = null;
      previousBeatData = null;
      nextBeatData = null;

      return result;
    } catch (error) {
      console.error('Failed to download first beat: ', error);
      return null;
    }
  },
  downloadBeatListByDay: async (offset, ecgDataMap) => {
    try {
      const dailyBeatData = [];

      let allBeatData = await Promise.all(fileUrls.map((url, i) => getAdjacentFileBeatData(i)));

      for (let i = 0; i < allBeatData.length; i++) {
        const currentBeatData = allBeatData[i];
        let previousBeatData = null;
        let nextBeatData = null;

        const { start, stop } = studySummariesJson[i];
        const startDate = new Date(start);
        const stopDate = new Date(stop);

        const isNearStart = (epoch) => epoch <= (startDate.getTime() + offset);
        const isNearEnd = (epoch) => epoch >= (stopDate.getTime() - offset);

        if (isNearStart(currentBeatData.beatEpochs[0])) {
          previousBeatData = i > 0 ? allBeatData[i - 1] : null;
        }

        if (isNearEnd(currentBeatData.beatEpochs[currentBeatData.beatEpochs.length - 1])) {
          nextBeatData = i < allBeatData.length - 1 ? allBeatData[i + 1] : null;
        }

        let dateEntry = dailyBeatData.find(entry => entry.date === startDate.toISOString().split('T')[0]);
        if (!dateEntry) {
          dateEntry = { date: startDate.toISOString().split('T')[0], hours: [] };
          dailyBeatData.push(dateEntry);
        }

        let hourEntry = dateEntry.hours.find(entry => entry.hour === startDate.getHours());
        if (!hourEntry) {
          hourEntry = { hour: startDate.getHours(), value: [] };
          dateEntry.hours.push(hourEntry);
        }

        for (const beatEpoch of currentBeatData.beatEpochs) {
          let beatMarkers = [];
          let hesBeatStatus = [];
          let ecgData = [];

          const currentData = mergeBeatData(currentBeatData, beatEpoch, offset);
          beatMarkers = beatMarkers.concat(currentData.beatMarkers);
          hesBeatStatus = hesBeatStatus.concat(currentData.hesBeatStatus);
          await downloadECGDataByBeatTime(beatEpoch, ecgDataMap);
          console.log('CACHE_ECG: ', CACHE_ECG);
          ecgData = ecgData.concat(await handleDownloadData({
            ecgDataMap,
            beat: { id: beatEpoch },
            offset,
            dataLength: ecgDataMap.samplingFrequency * 2,
          }));

          if (previousBeatData && isNearStart(beatEpoch)) {
            const previousData = mergeBeatData(previousBeatData, beatEpoch, offset);
            beatMarkers = beatMarkers.concat(previousData.beatMarkers);
            hesBeatStatus = hesBeatStatus.concat(previousData.hesBeatStatus);
          }

          if (nextBeatData && isNearEnd(beatEpoch)) {
            const nextData = mergeBeatData(nextBeatData, beatEpoch, offset);
            beatMarkers = beatMarkers.concat(nextData.beatMarkers);
            hesBeatStatus = hesBeatStatus.concat(nextData.hesBeatStatus);
          }

          hourEntry.value.push({
            beatEpochs: beatEpoch,
            beatMarkers,
            hesBeatStatus,
            ecgData
          });

          // Free memory
          beatMarkers = null;
          hesBeatStatus = null;
          ecgData = null;
        }

        const date = dateEntry.date;
        self.postMessage({ date, hourEntry });

        // Free memory
        hourEntry = null;
        dateEntry = null;
      }

      self.postMessage({ dailyBeatData });
      return dailyBeatData;
    } catch (error) {
      console.error('Failed to download beat data by day: ', error);
      return [];
    }
  },
});
import axios from 'axios';
import { expose } from 'threads/worker';
import _, { chunk } from 'lodash';
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
import TTLCache from '@isaacs/ttlcache';
import studySummariesJson from '../../../../dummyData/studySummaries.json';

dayjs.extend(isBetween);

const fileUrls = [
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-08-08-14%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-09-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-10-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-11-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-12-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-13-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-14-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-15-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-16-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-17-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-18-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-19-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-20-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-21-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-22-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-02-28-21-23-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-00-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-01-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-02-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-03-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-04-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-05-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-06-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-07-00-00%2B28-final.beat',
  'https://public.cdn.octomed.vn/6618e8f41638b377423eeb2f/beats/data-hourly-03-01-21-08-00-00%2B28-final.beat',
];

const CACHE_ECG = new TTLCache({ ttl: 3600000 }); // Cache ECG files for 1 hour
const CACHE_BEAT = new TTLCache({ ttl: 3600000 }); // Cache BEAT files for 1 hour
const config = {};

const downloadFile = async (url, cache) => {
  if (cache.has(url)) {
    return cache.get(url);
  }

  try {
    const response = await axios({
      method: 'GET',
      url,
      responseType: 'arraybuffer',
    });

    cache.set(url, response.data);
    return response.data;
  } catch (error) {
    console.error('Failed to download file: ', error);
    return null;
  }
};

const getECGDataForHour = async (hourIndex, ecgDataMap) => {
  if (hourIndex < 0 || hourIndex >= ecgDataMap.data.length) {
    return null;
  }
  const ecgDataEntry = ecgDataMap.data[hourIndex];
  const urlECG = `${ecgDataEntry.dataUrl}?${ecgDataMap.querySignature}`;
  const fileData = await downloadFile(urlECG, CACHE_ECG);
  if (!fileData) return null;

  return {
    start: ecgDataEntry.start,
    stop: ecgDataEntry.stop,
    data: fileData,
  };
};

const getFileIndexByTimeFirstBeat = (startTime, endTime, studySummaries) => {
  for (let i = 0; i < studySummaries.length; i++) {
    const entry = studySummaries[i];
    const entryStart = dayjs(entry.start);
    const entryStop = dayjs(entry.stop);
    if (dayjs(startTime).isBetween(entryStart, entryStop, null, '[]') ||
        dayjs(endTime).isBetween(entryStart, entryStop, null, '[]')) {
      return i;
    }
  }
  return -1;
};

const getFileIndexByTime = (time, ecgDataMap) => {
  for (let i = 0; i < ecgDataMap.data.length; i++) {
    const entry = ecgDataMap.data[i];
    const startTime = dayjs(entry.start.$d);
    const stopTime = dayjs(entry.stop.$d);
    if (dayjs(time).isBetween(startTime, stopTime, null, '[]')) {
      return i;
    }
  }
  return -1;
};

const manageECGCache = async (currentHourIndex, ecgDataMap) => {
  const hoursToDownload = [currentHourIndex - 1, currentHourIndex, currentHourIndex + 1];
  const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

  // Tải các file ECG cần thiết và xóa các file không cần thiết
  for (const hour of validHours) {
    await getECGDataForHour(hour, ecgDataMap);
  }

  const keysToDelete = [];
  for (const [key, value] of CACHE_ECG.entries()) {
    const urlHour = parseInt(key.split('data-hourly-')[1].split('-')[0]);
    if (!validHours.includes(urlHour)) {
      keysToDelete.push(key);
    }
  }

  if (keysToDelete.length > 0) {
    CACHE_ECG.delete(keysToDelete[0]);
  }
};

const downloadECGDataByBeatTime = async (beatTime, ecgDataMap) => {
  try {
    const beatTimestamp = dayjs(beatTime).valueOf();
    const fileIndex = getFileIndexByTime(beatTimestamp, ecgDataMap);

    if (fileIndex === -1) {
      throw new Error('No file found for the given beat time');
    }

    await manageECGCache(fileIndex, ecgDataMap);

    const ecgDataList = [];
    const hoursToDownload = [fileIndex - 1, fileIndex, fileIndex + 1];
    const validHours = hoursToDownload.filter(hour => hour >= 0 && hour < ecgDataMap.data.length);

    for (const hour of validHours) {
      const ecgData = await getECGDataForHour(hour, ecgDataMap);
      if (ecgData) {
        ecgDataList.push(ecgData);
      }
    }

    return ecgDataList;
  } catch (error) {
    console.error('Failed to download ECG data by beat time: ', error);
    return [];
  }
};

const mergeBeatData = (beatData, beatEpoch, offset) => {
  const beatMarkers = [];
  const hesBeatStatus = [];

  if (beatData) {
    beatData.beatEpochs.forEach((epoch, index) => {
      if (epoch >= (beatEpoch - offset) && epoch <= (beatEpoch + offset)) {
        const sample = Math.round(
          ((epoch - (beatEpoch - offset)) * config.samplingFrequency) / 1000,
        );
        beatMarkers.push(sample);
        hesBeatStatus.push(beatData.hesBeatStatus[index]);
      }
    });
  }

  return { beatMarkers, hesBeatStatus };
};

const downloadSelectedEcgData = async (ranges, querySignature) => {
  try {
    if (ranges.length) {
      const blob = [];
      for (const range of ranges) {
        const cacheKey = `${range.dataUrl}?${querySignature}`;
        if (CACHE_ECG.has(cacheKey)) {
          const cachedData = CACHE_ECG.get(cacheKey);
          if (range.dataRange.start >= 0 && range.dataRange.stop <= cachedData.byteLength) {
            const slicedData = cachedData.slice(range.dataRange.start, range.dataRange.stop + 1);
            blob.push(slicedData);
          } else {
            console.error('Invalid range:', range.dataRange);
          }
        } else {
          console.error('No cached data for URL:', range.dataUrl);
        }
      }
      let concatenated = await new Blob(blob).arrayBuffer();
      const { byteLength } = concatenated;
      if (byteLength % 2 !== 0) {
        concatenated = concatenated.slice(0, byteLength - 1);
      }

      return new Int16Array(concatenated);
    }
    return [];
  } catch (error) {
    console.error('Failed to download selected ECG data: ', error);
    return [];
  }
};

const convertBinaryToECGData = (totalChannel, binaryData) => {
  if (!binaryData) {
    return undefined;
  }
  const ecgData = Array.from({ length: totalChannel }, () => []);
  for (let i = 0; i < binaryData.length; i += totalChannel) {
    for (let j = 0; j < totalChannel; j++) {
      ecgData[j].push(binaryData[i + j]);
    }
  }
  return ecgData;
};

const addSineWaveToEcgData = (selectedEcgData, ranges, startMoment, stopMoment, ecgDataMap, dataLength = 75000) => {
  if (selectedEcgData