import {logger} from '../../src/utils/logger.js';
import {precisionRound} from '../../src/utils/string.js';
import {custom} from '../../src/app/custom.js';
import {
  AppOptions,
  App
} from '../../src/app/application.js';
import {WindowLevel} from '../../src/image/windowLevel.js';
import {getAsSimpleElements} from '../../src/dicom/dicomTag.js';
import {getSRContent} from '../../src/dicom/dicomSRContent.js';
import {getDwvVersion} from '../../src/dicom/dicomParser.js';
import {Point} from '../../src/math/point.js';
import {Matrix33} from '../../src/math/matrix.js';
import {getURLsFromKeyValueUri} from '../../src/utils/uri.js';

import {LayoutProtocol} from './viewer.ui.js';
import {DataTableUI} from './viewer.ui.datatable.js';
import {setupRenderTests} from './viewer.rendertest.js';
import {AnnotationUI} from './viewer.ui.annot.js';
import {SegmentationUI} from './viewer.ui.segment.js';
import {DrawToolUI} from './viewer.ui.draw.js';
import {BrushToolUI} from './viewer.ui.brush.js';
import {overlayConfig} from './overlays.js';

// doc imports
/* eslint-disable no-unused-vars */
import {ViewConfig} from '../../src/app/application.js';
/* eslint-enable no-unused-vars */


// global vars

/**
 * @type {App}
 */
let _app;

let _tools;
const _toolFeaturesUI = {};

// layout related

/**
 * Select all: returns true.
 *
 * @returns {boolean} True if selected.
 */
function selectAll(/*id*/) {
  return true;
}
/**
 * Select event ids.
 *
 * @param {string} id The id to check.
 * @returns {boolean} True if selected.
 */
function selectEven(id) {
  return parseInt(id, 10) % 2 === 0;
}
/**
 * Select odd ids.
 *
 * @param {string} id The id to check.
 * @returns {boolean} True if selected.
 */
function selectOdd(id) {
  return parseInt(id, 10) % 2 !== 0;
}
/**
 * Select CT modality data.
 *
 * @param {string} id The id to check.
 * @returns {boolean} True if selected.
 */
// function selectCT(id) {
//   const modality = _app.getMetaData(id)['00080060'].value[0];
//   return modality === 'CT';
// }
/**
 * Select PET modality data.
 *
 * @param {string} id The id to check.
 * @returns {boolean} True if selected.
 */
// function selectPET(id) {
//   const modality = _app.getMetaData(id)['00080060'].value[0];
//   return modality === 'PT';
// }

// single div
const layoutConfig0 = {
  id: '1*1',
  displaySets: [
    {
      divId: 'layerGroup0',
      dataSelector: selectAll,
    }
  ]
};
// one line, two columns
const layoutConfig1 = {
  id: '1*2',
  displaySets: [
    {
      divId: 'layerGroup0',
      dataSelector: selectEven,
    },
    {
      divId: 'layerGroup1',
      dataSelector: selectOdd,
    }
  ]
};
// MPR
const layoutConfig2 = {
  id: 'mpr',
  displaySets: [
    {
      divId: 'layerGroup0',
      orientation: 'axial',
      dataSelector: selectAll,
    },
    {
      divId: 'layerGroup1',
      orientation: 'coronal',
      dataSelector: selectAll,
    },
    {
      divId: 'layerGroup2',
      orientation: 'sagittal',
      dataSelector: selectAll,
    }
  ]
};

/**
 * @type {LayoutProtocol[]}
 */
const _layouts = [];
_layouts.push(new LayoutProtocol(layoutConfig0));
_layouts.push(new LayoutProtocol(layoutConfig1));
_layouts.push(new LayoutProtocol(layoutConfig2));

let _selectedLayoutIndex = 0;

/**
 * Setup simple dwv app.
 */
function viewerSetup() {
  // logger level (optional)
  logger.level = logger.levels.DEBUG;

  // example wl preset override
  custom.wlPresets = {
    PT: {
      'suv5-10': new WindowLevel(5, 10),
      'suv6-8': new WindowLevel(6, 8)
    }
  };

  // // example labelText override
  // custom.labelTexts = {
  //   rectangle: {
  //     '*': '{surface}!',
  //     MR: '{surface}!!'
  //   }
  // };

  // // example private logic for roi dialog
  // custom.openRoiDialog = function (meta, cb) {
  //   console.log('roi dialog', meta);
  //   const textExpr = prompt('[Custom dialog] Label', meta.textExpr);
  //   if (textExpr !== null) {
  //     meta.textExpr = textExpr;
  //     cb(meta);
  //   }
  // };

  // // example private logic for volume id value retrieval
  // custom.getVolumeIdTagValue = function (elements) {
  //   let value;
  //   const element = elements['ABCD0123'];
  //   if (typeof element !== 'undefined') {
  //     value = parseInt(element.value[0], 10);
  //   }
  //   return value;
  // };

  // // example private logic for pixel unit value retrieval
  // custom.getTagPixelUnit = function (/*elements*/) {
  //   return 'MyPixelUnit';
  // };

  // stage options
  const viewOnFirstLoadItem = true;

  // load counters
  let numberOfDataToLoad = 0;
  let numberOfLoadendData = 0;
  const dataLoadProgress = [];
  let firstRender = true;

  // tools
  _tools = {
    Scroll: {},
    WindowLevel: {},
    ZoomAndPan: {},
    Opacity: {},
    Draw: {options: [
      'Arrow',
      'Ruler',
      'Circle',
      'Ellipse',
      'Rectangle',
      'Protractor',
      'Roi'
    ]},
    Brush: {},
    Floodfill: {},
    Livewire: {},
    Filter: {options: [
      'Sharpen'
    ]}
  };

  // app config
  const options = new AppOptions();
  options.tools = _tools;
  options.viewOnFirstLoadItem = viewOnFirstLoadItem;
  options.overlayConfig = overlayConfig;
  // app
  _app = new App();
  _app.init(options);

  // abort shortcut handler
  const abortShortcut = function (event) {
    if (event.key === 'a') {
      _app.abortAllLoads();
    }
  };

  // bind events
  _app.addEventListener('error', function (event) {
    console.error('load error', event, event.error);
    const message = event.error.message;
    const isRenderError = typeof message !== 'undefined' &&
      message.startsWith('Render error');
    // abort load
    if (!isRenderError) {
      _app.abortLoad(event.dataid);
    }
  });
  _app.addEventListener('loadstart', function (event) {
    console.log('%c----------------', 'color: teal;');
    console.log('load source data:' + event.dataid, event.source);
    // timer
    console.time('load-data-' + event.dataid);
    // update load counters
    if (numberOfDataToLoad === numberOfLoadendData) {
      numberOfDataToLoad = 0;
      numberOfLoadendData = 0;
      // reset progress array
      dataLoadProgress.length = 0;
    }
    ++numberOfDataToLoad;
    // add abort shortcut
    window.addEventListener('keydown', abortShortcut);
    // remove post-load listeners
    if (_app.getDataIds().length !== 0) {
      removePostLoadListeners();
    }
  });
  const sumReducer = function (sum, value) {
    return sum + value;
  };
  _app.addEventListener('loadprogress', function (event) {
    if (typeof event.lengthComputable !== 'undefined' &&
      event.lengthComputable) {
      dataLoadProgress[event.dataid] =
        Math.ceil((event.loaded / event.total) * 100);
      const progressElement = document.getElementById('loadprogress');
      progressElement.value =
        dataLoadProgress.reduce(sumReducer) / numberOfDataToLoad;
    }
  });
  _app.addEventListener('loaditem', function (event) {
    if (typeof event.warn !== 'undefined') {
      console.warn('load-warn', event.warn);
    }
  });
  _app.addEventListener('loadstart', function (event) {
    // add new data view config for new data
    addNewDataViewConfig(event.dataid);
  });
  _app.addEventListener('loadend', function (event) {
    console.timeEnd('load-data-' + event.dataid);
    // update load counter
    ++numberOfLoadendData;
    // remove abort shortcut
    window.removeEventListener('keydown', abortShortcut);
  });
  _app.addEventListener('load', function (event) {
    // log meta data
    logMetaData(event.dataid, event.loadtype);
    // render if not done yet
    if (!viewOnFirstLoadItem) {
      _app.render(event.dataid);
    }
    // update sliders with new data info
    // (has to be after full load)
    initSliders(_layouts[_selectedLayoutIndex]);
    // add post-load listeners
    addPostLoadListeners();
  });

  // update UI at first render of first data
  const onRenderEnd = function (/*event*/) {
    if (firstRender) {
      // set app tool
      setAppTool();
      // enable html
      const elementIds = [
        'tools',
        'changelayout',
        'resetviews',
        'changesmoothing',
        'rotate-x',
        'rotate-y',
        'rotate-z',
        'rotate-match',
        'rotate-reset'
      ];
      for (const elementId of elementIds) {
        const element = document.getElementById(elementId);
        if (element) {
          element.disabled = false;
        }
      }
      // remove handler
      _app.removeEventListener('renderend', onRenderEnd);
      firstRender = false;
    }
  };
  // add handler (will be removed at first success)
  _app.addEventListener('renderend', onRenderEnd);

  _app.addEventListener('positionchange', function (event) {
    const input = document.getElementById('position');
    const values = event.value[1];
    let text = '(index: ' + event.value[0] + ')';
    if (event.value.length > 2) {
      text += ' value: ' + event.value[2];
    }
    input.value = values.map(getPrecisionRound(2));
    // index as small text
    const span = document.getElementById('positionspan');
    if (span) {
      span.innerHTML = text;
    }
    // update sliders' value
    updateSliders(_layouts[_selectedLayoutIndex]);
  });

  _app.addEventListener('filterrun', function (event) {
    console.log('filterrun', event);
  });
  _app.addEventListener('filterundo', function (event) {
    console.log('filterundo', event);
  });
  // labels
  _app.addEventListener('labelingstart', function (event) {
    console.time('label-data-' + event.dataid);
  });
  _app.addEventListener('labelschanged', function (event) {
    console.timeEnd('label-data-' + event.dataid);
  });
  // resampling
  _app.addEventListener('imageresamplingstart', function (event) {
    console.time('resample-data-' + event.dataid);
  });
  _app.addEventListener('imageresamplingcomplete', function (event) {
    console.timeEnd('resample-data-' + event.dataid);
  });

  _app.addEventListener('warn', function (event) {
    console.log('warn', event);
  });

  // default keyboard shortcuts
  window.addEventListener('keydown', function (event) {
    _app.defaultOnKeydown(event);

    // filter
    if (getSelectedToolName() === 'Filter' &&
      event.altKey && event.key === 'r') {
      // run the sharpen filter
      _app.setToolFeatures({
        filterName: 'Sharpen',
        runArgs: {
          dataId: _app.getDataIds()[0]
        },
        run: true
      });
    }
  });
  // default on resize
  window.addEventListener('resize', function () {
    _app.onResize();
  });

  // tool features UI
  const toolsUI = {
    Draw: DrawToolUI,
    Brush: BrushToolUI
  };

  for (const toolName in _tools) {
    if (typeof toolsUI[toolName] !== 'undefined') {
      const toolUI = new toolsUI[toolName](_app, _tools[toolName]);
      toolUI.registerListeners();
      _toolFeaturesUI[toolName] = toolUI;
    }
  }

  // data model UI
  const dataModelUI = {
    annotation: AnnotationUI,
    segmentation: SegmentationUI
  };

  for (const dmName in dataModelUI) {
    const dmUI = new dataModelUI[dmName](_app);
    dmUI.registerListeners();
  }

  const uriOptions = {};
  // uriOptions.batchSize = 100;
  // special dicom web cookie
  if (document.cookie) {
    const cookies = document.cookie.split('; ');
    // accept
    const acceptItem = cookies.find((item) => item.startsWith('accept='));
    if (typeof acceptItem !== 'undefined') {
      // accept is encoded in dcmweb.js (allows for ';')
      const accept = decodeURIComponent(acceptItem.split('=')[1]);
      if (typeof accept !== 'undefined' && accept.length !== 0) {
        uriOptions.requestHeaders = [];
        uriOptions.requestHeaders.push({
          name: 'Accept',
          value: accept
        });
      }
      // clean up
      document.cookie = 'accept=';
    }
    // token
    const tokenItem = cookies.find((item) => item.startsWith('access_token='));
    if (typeof tokenItem !== 'undefined') {
      const token = tokenItem.split('=')[1];
      if (typeof token !== 'undefined' && token.length !== 0) {
        if (typeof uriOptions.requestHeaders === 'undefined') {
          uriOptions.requestHeaders = [];
        }
        uriOptions.requestHeaders.push({
          name: 'Authorization',
          value: 'Bearer ' + token
        });
      }
      // clean up
      document.cookie = 'access_token=';
    }
  }
  // optional load from window location
  const urls = getURLsFromKeyValueUri(window.location.href);
  if (typeof urls !== 'undefined') {
    _app.loadURLs(urls, uriOptions);
  }
}

/**
 * Log meta data.
 *
 * @param {string} dataId The data ID.
 * @param {string} loadType The load type.
 */
function logMetaData(dataId, loadType) {
  // meta data
  const meta = _app.getMetaData(dataId);

  // log tags for data with transfer syntax (dicom)
  if (typeof meta['00020010'] !== 'undefined') {
    console.log('metadata data:' + dataId, getAsSimpleElements(meta));
  } else {
    console.log('metadata data:' + dataId, meta);
  }

  // get modality
  let modality;
  if (loadType === 'image' &&
    typeof meta['00080060'] !== 'undefined') {
    modality = meta['00080060'].value[0];
  }
  // log DICOM SEG
  if (modality === 'SEG') {
    logFramePosPats(meta);
  }
  // log DICOM SR
  if (modality === 'SR') {
    console.log('DICOM SR');
    const srContent = getSRContent(meta);
    console.log(srContent.toString());
  }
}

/**
 * Init individual slider on layer related event.
 * WARNING: needs to be called with the final geometry.
 *
 * @param {object} event The layer event.
 */
function initSliderOnEvent(event) {
  initSlider(event.layergroupid);
}

/**
 * Add post-load event listeners.
 */
function addPostLoadListeners() {
  // post-load since sliders need the full geometry
  _app.addEventListener('viewlayeradd', initSliderOnEvent);
  _app.addEventListener('drawlayeradd', initSliderOnEvent);
  _app.addEventListener('layerremove', initSliderOnEvent);
}

/**
 * Remove post-load event listeners.
 */
function removePostLoadListeners() {
  _app.removeEventListener('viewlayeradd', initSliderOnEvent);
  _app.removeEventListener('drawlayeradd', initSliderOnEvent);
  _app.removeEventListener('layerremove', initSliderOnEvent);
}

/**
 * Merge with first app config.
 *
 * @param {Object<string, ViewConfig[]>} dataViewConfigs A list
 *   of data view configs.
 */
function mergeAppConfig(dataViewConfigs) {
  for (const dataId in dataViewConfigs) {
    const configs = dataViewConfigs[dataId];
    const appConfigs = _app.getViewConfigs(dataId);
    if (appConfigs.length !== 0) {
      // use first as base
      for (const config of configs) {
        mergeConfigs(config, appConfigs[0]);
      }
    }
  }
}

/**
 * Merge a data config into the first input one.
 * Copies all but the divId and orientation property.
 *
 * @param {ViewConfig} config The config where to merge.
 * @param {ViewConfig} configToMerge The config to merge.
 * @returns {ViewConfig} The updated config.
 */
function mergeConfigs(config, configToMerge) {
  for (const key in configToMerge) {
    if (key !== 'divId' &&
      key !== 'orientation') {
      config[key] = configToMerge[key];
    }
  }
  return config;
}

/**
 * Get the slider for a given layer group.
 *
 * @param {number} layerGroupDivId The div id.
 * @returns {HTMLInputElement} The slider as html range.
 */
function getSlider(layerGroupDivId) {
  const range = document.createElement('input');
  range.type = 'range';
  range.className = 'vertical-slider';
  range.id = layerGroupDivId + '-slider';
  range.min = 0;
  range.max = 0;
  range.disabled = true;
  // update app on slider change
  range.oninput = function () {
    const lg = _app.getLayerGroupByDivId(layerGroupDivId);
    const ph = lg.getPositionHelper();
    const pos = ph.getCurrentPositionAtScrollValue(this.value);
    ph.setCurrentPosition(pos);
  };
  return range;
}

/**
 * Init sliders: show them and set max.
 *
 * @param {LayoutProtocol} layout The current layout.
 */
function initSliders(layout) {
  for (const divId of layout.getLayerGroupDivIds()) {
    initSlider(divId);
  }
}

/**
 * Init individual slider.
 * WARNING: needs to be called with the final geometry.
 *
 * @param {string} layerGroupId The id of the layer group.
 */
function initSlider(layerGroupId) {
  const slider = document.getElementById(layerGroupId + '-slider');
  if (slider) {
    // disabled by default
    slider.disabled = true;
    // init if possible
    const lg = _app.getLayerGroupByDivId(layerGroupId);
    if (typeof lg !== 'undefined') {
      const ph = lg.getPositionHelper();
      if (typeof ph !== 'undefined') {
        const max = ph.getMaximumScrollValue();
        if (max !== 0) {
          slider.disabled = false;
          slider.max = max;
          slider.value = ph.getCurrentPositionScrollValue();
        }
      }
    }
  }
}

/**
 * Update sliders: set the slider value to the current scroll index.
 *
 * @param {LayoutProtocol} layout The current layout.
 */
function updateSliders(layout) {
  for (const divId of layout.getLayerGroupDivIds()) {
    const slider = document.getElementById(divId + '-slider');
    if (slider) {
      const lg = _app.getLayerGroupByDivId(divId);
      if (typeof lg !== 'undefined') {
        const ph = lg.getPositionHelper();
        slider.value = ph.getCurrentPositionScrollValue();
      }
    }
  }
}

/**
 * Append a layer div in the root 'dwv' one.
 *
 * @param {string} id The id of the layer.
 */
function addLayerGroupDiv(id) {
  const layerDiv = document.createElement('div');
  layerDiv.id = id;
  layerDiv.className = 'layerGroup';

  const root = document.getElementById('dwv');
  root.appendChild(layerDiv);
  root.appendChild(getSlider(id));
}

/**
 * Add Layer Groups divs.
 *
 * @param {LayoutProtocol} layout The current layout.
 */
function addLayerGroupsDivs(layout) {
  // clean up
  const dwvDiv = document.getElementById('dwv');
  if (dwvDiv) {
    dwvDiv.innerHTML = '';
  }
  // add div
  for (const divId of layout.getLayerGroupDivIds()) {
    addLayerGroupDiv(divId);
  }
}

/**
 * Add data view config for the input data.
 *
 * @param {string} dataId The data ID.
 */
function addNewDataViewConfig(dataId) {
  const configs = _layouts[_selectedLayoutIndex].getViewConfigsByDataId(dataId);
  for (const config of configs) {
    _app.addDataViewConfig(dataId, config, false);
  }
}

/**
 * Setup file line.
 */
function setupFileLine() {
  // bind app to input files
  const fileinput = document.getElementById('fileinput');
  fileinput.addEventListener('change', function (event) {
    const files = event.target.files;
    if (files.length !== 0) {
      _app.loadFiles(files);
    } else {
      throw new Error('No files to load');
    }
  });
}

/**
 * Setup position line.
 */
function setupPositionLine() {
  const positionInput = document.getElementById('position');
  positionInput.addEventListener('change', function (event) {
    const vls = _app.getViewLayersByDataId('0');
    const vc = vls[0].getViewController();
    const element = event.target;
    const values = element.value.split(',');
    const point = new Point(values.map(parseFloat));
    vc.setCurrentPosition(point);
  });
}

/**
 * Setup layout line.
 *
 * @param {DataTableUI} dataTable The associated data table.
 */
function setupLayoutLine(dataTable) {
  const resetViewsButton = document.getElementById('resetviews');
  resetViewsButton.disabled = true;
  resetViewsButton.addEventListener('click', function () {
    _app.resetZoomPan();
  });

  const changeLayoutSelect = document.getElementById('changelayout');
  changeLayoutSelect.disabled = true;
  for (const layout of _layouts) {
    const layoutId = layout.getId();
    const option = document.createElement('option');
    option.value = layoutId;
    if (layoutId === _layouts[_selectedLayoutIndex].getId()) {
      option.selected = true;
    }
    option.appendChild(document.createTextNode(layout.getId()));
    changeLayoutSelect.appendChild(option);
  }
  changeLayoutSelect.addEventListener('change', function (event) {
    const selectElement = event.target;
    const layoutId = selectElement.value;
    const layoutIndex = _layouts.findIndex(
      (elem) => elem.getId() === layoutId
    );
    if (layoutIndex === -1) {
      throw new Error('Unknown layout: ' + layoutId);
    } else {
      _selectedLayoutIndex = layoutIndex;
    }
    const selectedLayout = _layouts[_selectedLayoutIndex];

    // add layer groups div
    addLayerGroupsDivs(selectedLayout);

    // get configs
    const dataIds = _app.getDataIds();
    const dataViewConfigs = selectedLayout.getDataViewConfigs(dataIds);

    // merge app configs for possible extras (like window level)
    mergeAppConfig(dataViewConfigs);

    // clear data table
    dataTable.unRegisterViewListeners();
    dataTable.clearDataTable();

    // update layout for data table
    dataTable.unRegisterLayerAddListeners();
    dataTable.registerLayerAddListeners(selectedLayout);

    // set config (deletes previous layers)
    _app.setDataViewConfigs(dataViewConfigs);

    // render data (creates layers)
    for (let i = 0; i < dataIds.length; ++i) {
      _app.render(dataIds[i]);
    }

    // listen to view changes
    dataTable.registerViewListeners();

    // need to set tool after config change
    setAppTool();
  });

  const smoothingChk = document.getElementById('changesmoothing');
  smoothingChk.checked = false;
  smoothingChk.disabled = true;
  smoothingChk.addEventListener('change', function (event) {
    const inputElement = event.target;
    _app.setImageSmoothing(inputElement.checked);
  });
}

/**
 * Setup rotate line.
 */
function setupRotateLine() {
  const rotateXButton = document.getElementById('rotate-x');
  rotateXButton.disabled = true;
  rotateXButton.addEventListener('click', function () {
    const lg = _app.getLayerGroupByDivId('layerGroup0');
    const vl = lg.getBaseViewLayer();
    const dataId = vl.getDataId();

    const image = _app.getImage(dataId);
    const geometry = image.getGeometry();

    const angle = (Math.PI / 180) * 20;

    /* eslint-disable @stylistic/js/array-element-newline */
    const rotation = new Matrix33([
      1, 0, 0,
      0, Math.cos(angle), -Math.sin(angle),
      0, Math.sin(angle), Math.cos(angle)
    ]);
    /* eslint-enable @stylistic/js/array-element-newline */

    const newOrientation = rotation.multiply(geometry.getOrientation());

    _app.resample(dataId, newOrientation);
  });

  const rotateYButton = document.getElementById('rotate-y');
  rotateYButton.disabled = true;
  rotateYButton.addEventListener('click', function () {
    const lg = _app.getLayerGroupByDivId('layerGroup0');
    const vl = lg.getBaseViewLayer();
    const dataId = vl.getDataId();

    const image = _app.getImage(dataId);
    const geometry = image.getGeometry();

    const angle = (Math.PI / 180) * 20;

    /* eslint-disable @stylistic/js/array-element-newline */
    const rotation = new Matrix33([
      Math.cos(angle), 0, -Math.sin(angle),
      0, 1, 0,
      Math.sin(angle), 0, Math.cos(angle)
    ]);
    /* eslint-enable @stylistic/js/array-element-newline */

    const newOrientation = rotation.multiply(geometry.getOrientation());

    _app.resample(dataId, newOrientation);
  });

  const rotateZButton = document.getElementById('rotate-z');
  rotateZButton.disabled = true;
  rotateZButton.addEventListener('click', function () {
    const lg = _app.getLayerGroupByDivId('layerGroup0');
    const vl = lg.getBaseViewLayer();
    const dataId = vl.getDataId();

    const image = _app.getImage(dataId);
    const geometry = image.getGeometry();

    const angle = (Math.PI / 180) * 20;

    /* eslint-disable @stylistic/js/array-element-newline */
    const rotation = new Matrix33([
      Math.cos(angle), -Math.sin(angle), 0,
      Math.sin(angle), Math.cos(angle), 0,
      0, 0, 1
    ]);
    /* eslint-enable @stylistic/js/array-element-newline */

    const newOrientation = rotation.multiply(geometry.getOrientation());

    _app.resample(dataId, newOrientation);
  });

  const rotateMatchButton = document.getElementById('rotate-match');
  rotateMatchButton.disabled = true;
  rotateMatchButton.addEventListener('click', function () {
    const dataIds = _app.getDataIds();
    if (dataIds.length < 2) {
      console.log('Not enough datas to match geometries');
    }
    // log geometries if resample is needed
    const geometry0 = _app.getData(dataIds[0]).image.getGeometry();
    const geometry1 = _app.getData(dataIds[1]).image.getGeometry();
    if (!geometry0.getOrientation().equals(geometry1.getOrientation())) {
      console.log('Resample match');
      console.log('geometry0', geometry0.toString());
      console.log('geometry1', geometry1.toString());
    }
    // resample match data 0 and 1
    _app.resampleMatch(dataIds[0], dataIds[1]);
  });

  const rotateResetButton = document.getElementById('rotate-reset');
  rotateResetButton.disabled = true;
  rotateResetButton.addEventListener('click', function () {
    const lg = _app.getLayerGroupByDivId('layerGroup0');
    const vl = lg.getBaseViewLayer();
    const dataId = vl.getDataId();

    _app.revertResample(dataId);
  });
}

/**
 * Setup the binders checkboxes.
 */
function setupBindersCheckboxes() {
  const propList = [
    'WindowLevel',
    'Position',
    'Zoom',
    'Offset',
    'Opacity',
    'ColourMap',
    'MaskView'
  ];
  const binders = [];
  // add all binders at startup
  for (let b = 0; b < propList.length; ++b) {
    binders.push(propList[b] + 'Binder');
  }
  _app.setLayerGroupsBinders(binders);

  /**
   * Add a binder.
   *
   * @param {string} propName The name of the property to bind.
   */
  function addBinder(propName) {
    binders.push(propName + 'Binder');
    _app.setLayerGroupsBinders(binders);
  }
  /**
   * Remove a binder.
   *
   * @param {string} propName The name of the property to bind.
   */
  function removeBinder(propName) {
    const index = binders.indexOf(propName + 'Binder');
    if (index !== -1) {
      binders.splice(index, 1);
    }
    _app.setLayerGroupsBinders(binders);
  }
  /**
   * Get the input change handler for a binder.
   *
   * @param {string} propName The name of the property to bind.
   * @returns {object} The handler.
   */
  function getOnInputChange(propName) {
    return function (event) {
      const inputElement = event.target;
      if (inputElement.checked) {
        addBinder(propName);
      } else {
        removeBinder(propName);
      }
    };
  }

  const fieldset = document.getElementById('binders');

  // individual binders
  for (let i = 0; i < propList.length; ++i) {
    const propName = propList[i];

    const input = document.createElement('input');
    input.id = 'binder-' + i;
    input.type = 'checkbox';
    input.checked = true;
    input.onchange = getOnInputChange(propName);

    const label = document.createElement('label');
    label.htmlFor = input.id;
    label.appendChild(document.createTextNode(propName));

    fieldset.appendChild(input);
    fieldset.appendChild(label);
  }

  // check all
  const allInput = document.createElement('input');
  allInput.id = 'binder-all';
  allInput.type = 'checkbox';
  allInput.checked = true;
  allInput.onchange = function () {
    for (let j = 0; j < propList.length; ++j) {
      document.getElementById('binder-' + j).click();
    }
  };
  const allLabel = document.createElement('label');
  allLabel.htmlFor = allInput.id;
  allLabel.appendChild(document.createTextNode('all'));
  fieldset.appendChild(allInput);
  fieldset.appendChild(allLabel);
}

/**
 * Setup the tools checkboxes.
 */
function setupToolsCheckboxes() {
  const getChangeTool = function (tool) {
    return function () {
      setAppTool(tool);
    };
  };

  const getKeyCheck = function (char, input) {
    return function (event) {
      if (!event.ctrlKey &&
        !event.altKey &&
        !event.shiftKey &&
        event.key === char) {
        input.click();
      }
    };
  };

  const fieldset = document.getElementById('tools');
  const keys = Object.keys(_tools);
  for (let i = 0; i < keys.length; ++i) {
    const key = keys[i];

    const input = document.createElement('input');
    input.name = 'tools';
    input.type = 'radio';
    input.id = 'tool-' + i;
    input.title = key;
    input.onchange = getChangeTool(key);

    // select first one
    if (i === 0) {
      input.checked = true;
    }

    const label = document.createElement('label');
    label.htmlFor = input.id;
    label.title = input.title;
    label.appendChild(document.createTextNode(input.title));

    fieldset.appendChild(input);
    fieldset.appendChild(label);

    // keyboard shortcut
    const shortcut = key[0].toLowerCase();
    window.addEventListener('keydown', getKeyCheck(shortcut, input));
  }

  // tool options
  const div = document.createElement('div');
  div.id = 'toolOptions';
  fieldset.appendChild(div);
}

/**
 * Set the app tool.
 *
 * @param {string} [toolName] The tool to set.
 */
function setAppTool(toolName) {
  // find the tool name if not provided
  if (typeof toolName === 'undefined') {
    const toolsInput = document.getElementsByName('tools');
    for (let j = 0; j < toolsInput.length; ++j) {
      const toolInput = toolsInput[j];
      if (toolInput.checked) {
        toolName = toolInput.title;
        break;
      }
    }
    if (typeof toolName === 'undefined') {
      console.warn('Cannot find tool to set the app with...');
      return;
    }
  }

  // set tool for app
  _app.setTool(toolName);

  // force window level non strict mode
  if (toolName === 'WindowLevel') {
    _app.setToolFeatures({
      strictViewLayer: false
    });
  }

  // clear options html
  const toolOptionsEl = document.getElementById('toolOptions');
  if (toolOptionsEl !== null) {
    toolOptionsEl.innerHTML = '';
  }
  // tool features
  const featuresUI = _toolFeaturesUI[toolName];
  if (toolOptionsEl !== null &&
    typeof featuresUI !== 'undefined') {
    // setup html
    const featuresHtml = featuresUI.getHtml();
    if (typeof featuresHtml !== 'undefined') {
      toolOptionsEl.appendChild(featuresHtml);
    }
    // pass value to app
    const features = featuresUI.getValue();
    if (typeof features !== 'undefined') {
      _app.setToolFeatures(features);
    }
  }
}

/**
 * Get the selected tool name.
 *
 * @returns {string|undefined} The tool name.
 */
function getSelectedToolName() {
  let res;
  const element = document.querySelector('input[name="tools"]:checked');
  if (element) {
    res = element.title;
  }
  return res;
}

/**
 * Compare two pos pat keys.
 *
 * @param {string} a The key of the first item.
 * @param {string} b The key of the second item.
 * @returns {number} Negative if a<b, positive if a>b.
 */
function comparePosPat(a, b) {
  const za = a.split('\\').at(-1);
  const zb = b.split('\\').at(-1);
  let res = 0;
  if (typeof za !== 'undefined' &&
    typeof zb !== 'undefined') {
    res = parseFloat(za) - parseFloat(zb);
  }
  return res;
}

/**
 * Sort an object with pos pat string keys.
 *
 * @param {object} obj The object to sort.
 * @returns {object} The sorted object.
 */
function sortByPosPatKey(obj) {
  const keys = Object.keys(obj);
  keys.sort(comparePosPat);
  const sorted = new Map();
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    sorted.set(key, obj[key]);
  }
  return sorted;
}

/**
 * Get a rounding function for a specific precision.
 *
 * @param {number} precision The rounding precision.
 * @returns {Function} The rounding function.
 */
function getPrecisionRound(precision) {
  return function (x) {
    return precisionRound(x, precision);
  };
}

/**
 * Log the DICCOM seg segments ordered by frame position patients.
 *
 * @param {object} elements The DICOM seg elements.
 */
function logFramePosPats(elements) {
  // PerFrameFunctionalGroupsSequence
  const perFrame = elements['52009230'].value;
  const perPos = {};
  for (let i = 0; i < perFrame.length; ++i) {
    // PlanePositionSequence
    const posSq = perFrame[i]['00209113'].value;
    // ImagePositionPatient
    const pos = posSq[0]['00200032'].value;
    if (typeof perPos[pos] === 'undefined') {
      perPos[pos] = [];
    }
    // FrameContentSequence
    const frameSq = perFrame[i]['00209111'].value;
    // DimensionIndexValues
    const dim = frameSq[0]['00209157'].value;
    perPos[pos].push(dim);
  }
  console.log('DICOM SEG Segments', sortByPosPatKey(perPos));
}

/**
 * Setup about line.
 */
function setupAbout() {
  const testsDiv = document.getElementById('about');
  const link = document.createElement('a');
  link.href = 'https://github.com/ivmartel/dwv';
  link.appendChild(document.createTextNode('dwv'));
  const text = document.createTextNode(
    ' v' + getDwvVersion() +
    ' on ' + navigator.userAgent);

  testsDiv.appendChild(link);
  testsDiv.appendChild(text);
}

// ---------------------------------------------

/**
 * Main setup.
 */
function setup() {
  // add layer groups div
  addLayerGroupsDivs(_layouts[_selectedLayoutIndex]);

  // viewer setup: creates app
  viewerSetup();

  // data table
  const dataTable = new DataTableUI(_app);
  dataTable.registerListeners();
  dataTable.registerLayerAddListeners(_layouts[_selectedLayoutIndex]);

  // setup
  setupFileLine();
  setupPositionLine();
  setupLayoutLine(dataTable);
  setupRotateLine();
  setupBindersCheckboxes();
  setupToolsCheckboxes();
  setupRenderTests(_app);
  setupAbout();
}

// launch
setup();
