import '../../Common/DataModel/ImageData.js';
import '../../Common/DataModel/PolyData.js';
import vtk from '../../vtk.js';
import { m as macro } from '../../macros2.js';
import DataAccessHelper from './DataAccessHelper.js';
import vtkDataArray from '../../Common/Core/DataArray.js';
import vtkStringArray from '../../Common/Core/StringArray.js';
import './DataAccessHelper/LiteHttpDataAccessHelper.js';

// For vtk factory
// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; // HTTP + zip
// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; // html + base64 + zip
// import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; // zip

const fieldDataLocations = ['pointData', 'cellData', 'fieldData'];
const ARRAY_BUILDERS = {
  vtkDataArray,
  vtkStringArray
};

// ----------------------------------------------------------------------------
// Global methods
// ----------------------------------------------------------------------------

const cachedArraysAndPromises = {};
const cachedArraysMetaData = {};
const MiB = 1024 * 1024;
const GEOMETRY_ARRAYS = {
  vtkPolyData(dataset) {
    const arrayToDownload = [];
    arrayToDownload.push(dataset.points);
    ['verts', 'lines', 'polys', 'strips'].forEach(cellName => {
      if (dataset[cellName]) {
        arrayToDownload.push(dataset[cellName]);
      }
    });
    return arrayToDownload;
  },
  vtkImageData(dataset) {
    return [];
  },
  vtkUnstructuredGrid(dataset) {
    const arrayToDownload = [];
    arrayToDownload.push(dataset.points);
    arrayToDownload.push(dataset.cells);
    arrayToDownload.push(dataset.cellTypes);
    return arrayToDownload;
  },
  vtkRectilinearGrid(dataset) {
    const arrayToDownload = [];
    arrayToDownload.push(dataset.xCoordinates);
    arrayToDownload.push(dataset.yCoordinates);
    arrayToDownload.push(dataset.zCoordinates);
    return arrayToDownload;
  }
};
function processDataSet(publicAPI, model, dataset, fetchArray, resolve, reject, loadData) {
  const enable = model.enableArray;

  // Generate array list
  model.arrays = [];
  fieldDataLocations.forEach(location => {
    if (dataset[location]) {
      dataset[location].arrays.map(i => i.data).forEach(array => {
        model.arrays.push({
          name: array.name,
          enable,
          location,
          array,
          registration: array.ref.registration || 'addArray'
        });
      });

      // Reset data arrays
      dataset[location].arrays = [];
    }
  });

  // Fetch geometry arrays
  const pendingPromises = [];
  const {
    progressCallback
  } = model;
  const compression = model.fetchGzip ? 'gz' : null;
  GEOMETRY_ARRAYS[dataset.vtkClass](dataset).forEach(array => {
    pendingPromises.push(fetchArray(array, {
      compression,
      progressCallback
    }));
  });
  function success() {
    model.dataset = vtk(dataset);
    if (!loadData) {
      model.output[0] = model.dataset;
      resolve(publicAPI, model.output[0]);
    } else {
      publicAPI.loadData().then(() => {
        model.output[0] = model.dataset;
        resolve(publicAPI, model.output[0]);
      });
    }
  }

  // Wait for all geometry array to be fetched
  if (pendingPromises.length) {
    Promise.all(pendingPromises).then(success, err => {
      reject(err);
    });
  } else {
    success();
  }
}

// ----------------------------------------------------------------------------
// vtkHttpDataSetReader methods
// ----------------------------------------------------------------------------

function vtkHttpDataSetReader(publicAPI, model) {
  // Set our className
  model.classHierarchy.push('vtkHttpDataSetReader');

  // Empty output by default
  model.output[0] = vtk({
    vtkClass: 'vtkPolyData'
  });

  // Create default dataAccessHelper if not available
  if (!model.dataAccessHelper) {
    model.dataAccessHelper = DataAccessHelper.get('http');
  }

  // Internal method to fetch Array
  function fetchArray(array) {
    let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    const arrayId = `${array.ref.id}|${array.vtkClass}`;
    if (!cachedArraysAndPromises[arrayId]) {
      // Cache the promise while fetching
      cachedArraysAndPromises[arrayId] = model.dataAccessHelper.fetchArray(publicAPI, model.baseURL, array, options).then(newArray => {
        // Remove promise and return here if caching is disabled
        if (!model.maxCacheSize) {
          delete cachedArraysAndPromises[arrayId];
          return newArray;
        }

        // Replace the promise with the array in cache once downloaded
        cachedArraysAndPromises[arrayId] = newArray;
        cachedArraysMetaData[arrayId] = {
          lastAccess: new Date()
        };

        // If maxCacheSize is set to -1 the cache is unlimited
        if (model.maxCacheSize < 0) {
          return newArray;
        }

        // sort cache according to access times (potentially needed for creating space)
        const cachedArrays = {};
        Object.keys(cachedArraysMetaData).forEach(arrId => {
          cachedArrays[arrId] = {
            array: cachedArraysAndPromises[arrId],
            lastAccess: cachedArraysMetaData[arrId].lastAccess
          };
        });
        const sortedArrayCache = Object.entries(cachedArrays).sort((a, b) => Math.sign(b[1].lastAccess - a[1].lastAccess));

        // Check cache size
        const cacheSizeLimit = model.maxCacheSize * MiB;
        let cacheSize = Object.values(cachedArrays).reduce((accSize, entry) => accSize + entry.array.values.byteLength, 0);

        // Delete cache entries until size is below the limit
        while (cacheSize > cacheSizeLimit && sortedArrayCache.length > 0) {
          const [oldId, entry] = sortedArrayCache.pop();
          delete cachedArraysAndPromises[oldId];
          delete cachedArraysMetaData[oldId];
          cacheSize -= entry.array.values.byteLength;
        }

        // Edge case: If the new entry is bigger than the cache limit
        if (!cachedArraysMetaData[arrayId]) {
          macro.vtkWarningMacro('Cache size is too small for the dataset');
        }
        return newArray;
      });
    } else {
      // cacheArrays[arrayId] can be a promise or value
      Promise.resolve(cachedArraysAndPromises[arrayId]).then(cachedArray => {
        if (array !== cachedArray) {
          //  Update last access for cache retention rules (if caching is enabled)
          if (model.maxCacheSize) {
            cachedArraysMetaData[arrayId].lastAccess = new Date();
          }

          // Assign cached array as result
          Object.assign(array, cachedArray);
          delete array.ref;
        }
      });
    }
    return Promise.resolve(cachedArraysAndPromises[arrayId]);
  }

  // Fetch dataset (metadata)
  publicAPI.updateMetadata = function () {
    let loadData = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
    if (model.compression === 'zip') {
      return new Promise((resolve, reject) => {
        DataAccessHelper.get('http').fetchBinary(model.url).then(zipContent => {
          model.dataAccessHelper = DataAccessHelper.get('zip', {
            zipContent,
            callback: zip => {
              model.baseURL = '';
              model.dataAccessHelper.fetchJSON(publicAPI, 'index.json').then(dataset => {
                publicAPI.parseObject(dataset, {
                  loadData,
                  deepCopy: false
                }).then(resolve, reject);
              }, error => {
                reject(error);
              });
            }
          });
        }, error => {
          reject(error);
        });
      });
    }
    return new Promise((resolve, reject) => {
      model.dataAccessHelper.fetchJSON(publicAPI, model.url).then(dataset => {
        publicAPI.parseObject(dataset, {
          loadData,
          deepCopy: false
        }).then(resolve, reject);
      }, error => {
        reject(error);
      });
    });
  };

  // Set DataSet url
  publicAPI.setUrl = function (url) {
    let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    if (url.indexOf('index.json') === -1 && !options.fullpath) {
      model.baseURL = url;
      model.url = `${url}/index.json`;
    } else {
      model.url = url;

      // Remove the file in the URL
      const path = url.split('/');
      path.pop();
      model.baseURL = path.join('/');
    }
    model.compression = options.compression;

    // Fetch metadata
    return publicAPI.updateMetadata(!!options.loadData);
  };
  publicAPI.parseObject = (manifest, _ref) => {
    let {
      loadData,
      baseUrl,
      deepCopy = true
    } = _ref;
    if (baseUrl) {
      model.baseURL = baseUrl;
    }
    const dataset = deepCopy ? structuredClone(manifest) : manifest;
    return new Promise((resolve, reject) => {
      processDataSet(publicAPI, model, dataset, fetchArray, resolve, reject, loadData);
    });
  };

  // Fetch the actual data arrays
  publicAPI.loadData = () => {
    const datasetObj = model.dataset;
    const arrayToFecth = model.arrays.filter(array => array.enable).filter(array => array.array.ref).map(array => array.array);
    return new Promise((resolve, reject) => {
      const error = e => {
        reject(e);
      };
      const processNext = () => {
        if (arrayToFecth.length) {
          const {
            progressCallback
          } = model;
          const compression = model.fetchGzip ? 'gz' : null;
          fetchArray(arrayToFecth.pop(), {
            compression,
            progressCallback
          }).then(processNext, error);
        } else if (datasetObj) {
          // Perform array registration on new arrays
          model.arrays.filter(metaArray => metaArray.registration && !metaArray.array.ref).forEach(metaArray => {
            const newArray = ARRAY_BUILDERS[metaArray.array.vtkClass].newInstance(metaArray.array);
            datasetObj[`get${macro.capitalize(metaArray.location)}`]()[metaArray.registration](newArray);
            delete metaArray.registration;
          });
          datasetObj.modified();
          resolve(publicAPI, datasetObj);
        }
      };

      // Start processing queue
      processNext();
    });
  };
  publicAPI.requestData = (inData, outData) => {
    // do nothing loadData will eventually load up the data
  };

  // Toggle arrays to load
  publicAPI.enableArray = function (location, name) {
    let enable = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
    const activeArray = model.arrays.filter(array => array.name === name && array.location === location);
    if (activeArray.length === 1) {
      activeArray[0].enable = enable;
    }
  };

  // return id's of cached arrays
  publicAPI.getCachedArrayIds = () => Object.keys(cachedArraysMetaData);

  // clear global array cache
  publicAPI.clearCache = () => Object.keys(cachedArraysMetaData).forEach(k => {
    delete cachedArraysAndPromises[k];
    delete cachedArraysMetaData[k];
  });

  // return Busy state
  publicAPI.isBusy = () => !!model.requestCount;
}

// ----------------------------------------------------------------------------
// Object factory
// ----------------------------------------------------------------------------

const DEFAULT_VALUES = {
  enableArray: true,
  fetchGzip: false,
  arrays: [],
  url: null,
  baseURL: null,
  requestCount: 0,
  arrayCachingEnabled: true,
  maxCacheSize: 2048
  // dataAccessHelper: null,
};

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

function extend(publicAPI, model) {
  let initialValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
  Object.assign(model, DEFAULT_VALUES, initialValues);

  // Build VTK API
  macro.obj(publicAPI, model);
  macro.get(publicAPI, model, ['enableArray', 'fetchGzip', 'url', 'baseURL', 'dataAccessHelper', 'maxCacheSize']);
  macro.set(publicAPI, model, ['dataAccessHelper', 'progressCallback', 'maxCacheSize']);
  macro.getArray(publicAPI, model, ['arrays']);
  macro.algo(publicAPI, model, 0, 1);
  macro.event(publicAPI, model, 'busy');

  // Object methods
  vtkHttpDataSetReader(publicAPI, model);

  // Make sure we can destructuring progressCallback from model
  if (model.progressCallback === undefined) {
    model.progressCallback = null;
  }
}

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

const newInstance = macro.newInstance(extend, 'vtkHttpDataSetReader');

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

var vtkHttpDataSetReader$1 = {
  newInstance,
  extend
};

export { vtkHttpDataSetReader$1 as default, extend, newInstance };
