const JSONLoader = require('./JSONLoader').JSONLoader;
const THREE = require('three');
const FileLoader = THREE.FileLoader;

const mergeGeometries = (geometries) => {
  const merge = (geometry1, geometry2) => {
    geometry1.merge(geometry2);
  }

  if (geometries && geometries.length > 0) {
    while (geometries.length > 1) {
      const geometry2 = geometries.splice(1,1);
      merge(geometries[0], geometry2[0]);
    }
    return geometries[0];
  }
  return undefined;
}

const IndexedSourcesHandler = function(urlIn, crossOrigin, onDownloadedCallback) {
  const loader = new FileLoader();
  const jsonLoader = new JSONLoader();
  loader.crossOrigin = crossOrigin;
  const url = urlIn;
  const onDownloaded = onDownloadedCallback;
  let data = undefined;
  let downloading = false;
  let finished = false;
  let error = undefined;
  const items = [];

  const processItemDownloaded = (item) => {
    const modelData = data[item.index];
    if (modelData) {
      let obj = jsonLoader.parse( modelData );
      item.onLoad(obj.geometry, obj.materials);
    } else {
      processItemError(item, {responseURL: url});
    }
  }

  const processItemError = (item) => {
    if (item.onError) {
      if (!error) {
        error = {responseURL: url};
      }
      item.onError(error);
    }
  }

  this.downloadCompleted = (args) => {
    try {
      data = JSON.parse(args[0]);
      downloading = false;
      finished = true;
      if (Array.isArray(data)) {
        items.forEach(item => processItemDownloaded(item));
      } else {
        items.forEach(item => processItemError(item));
      }
    } catch {
      items.forEach(item => processItemError(item));
    }
  }

  const errorHandling = () => {
    return xhr => {
      error = xhr;
      finished = true;
      downloading = false;
      items.forEach((item) => {
        processItemError(item);
      });
    }
  }

  const progressHandling = () => {
    return xhr => {
      items.forEach((item) => {
        if (item.onProgress) {
          item.onProgress(xhr);
        }
      });
    }
  }

  this.load = (index, onLoad, onProgress, onError) => {
    const item = {
      index,
      onLoad,
      onProgress,
      onError,
    };
    if (finished) {
      if (data) {
        processItemDownloaded(item);
      } else {
        processItemError(error);
      }
    } else if (downloading) {
      //quene it up
      items.push(item);
    } else {
      items.push(item);
      downloading = true;
      loader.load(url, onDownloaded, progressHandling, errorHandling);
    }
  }
}

const MultiSourcesHandler = function(numberIn, onLoadCallback) {
  const allData = [];
  const number = numberIn;
  const onLoad = onLoadCallback;
  let totalDownloaded = 0;

  this.itemDownloaded = (order, args) => {
    allData[order]= args;
    totalDownloaded++;
    if (totalDownloaded == number) {
      const materials = allData[0][1];
      const geometries = allData.map((data) => data[0]);
      //All geometries will be merged into the first one
      const geometry = mergeGeometries(geometries);
      for (let i = 1; i < number; i++) {
        allData[order][0].dispose();
        allData[order][1].forEach((material) => material.dispose());
      }
      onLoad(geometry, materials);
    }
  }
}

exports.PrimitivesLoader = function () {
  let concurrentDownloads = 0;
  const MAX_DOWNLOAD = 20;
  this.crossOrigin = "Anonymous";
  const loader = new JSONLoader();
  const waitingList = [];
  //URL to loader pair
  const indexedLoaders = {};

  //Load the first file then the rest will be handled separately
  const loadFromMultipleSources = (urls, onLoad, onProgress, onError, options) => {
    const number = urls.length;
    const msHandler = new MultiSourcesHandler(number, onLoad);
    //The order here will give us hint on the sequence on merging the primitives
    let order = 0;
    urls.forEach((url) => {
      const newOptions = options ? {...options} : {};
      newOptions.msHandler = msHandler;
      newOptions.order = order;
      order++;
      loadFromSingleSource(url, onLoad, onProgress, onError, newOptions);
    });
  }

  const handleIndexedSource = (url, onLoad, onProgress, onError, options) => {
    const newOptions = options ? {...options} : {};
    let indexedLoader = indexedLoaders[url];
    if (!indexedLoader) {
      if (MAX_DOWNLOAD > concurrentDownloads) {
        const onLoadCallback = new onFinally(undefined, this, newOptions);
        ++concurrentDownloads;
        indexedLoader = new IndexedSourcesHandler(url, this.crossOrigin, onLoadCallback);
        indexedLoaders[url] = indexedLoader;
      } else {
        waitingList.push({
          url,
          onLoad,
          onProgress,
          onError,
          options,
        });
      }
    }
    if (indexedLoader) {
      newOptions.isHandler = indexedLoader;
      indexedLoader.load(options.index, onLoad, onProgress, onError);
    }
  }

  const loadFromSingleSource = (url, onLoad, onProgress, onError, options) => {
    if (options && (options.index !== undefined) ) {
      handleIndexedSource(url, onLoad, onProgress, onError, options);
    } else {
      //Standard loading
      if (MAX_DOWNLOAD > concurrentDownloads) {
        ++concurrentDownloads;
        const onLoadCallback = new onFinally(onLoad, this, options);
        const onErrorCallback = new onFinally(onError, this, options);
        loader.crossOrigin = this.crossOrigin;
        loader.load(url, onLoadCallback, onProgress, onErrorCallback);
      } else {
        waitingList.push({
          url,
          onLoad,
          onProgress,
          onError,
          options,
        });
      }
    }
  }

  this.load = (url, onLoad, onProgress, onError, options) => {
    if (Array.isArray(url)) {
      loadFromMultipleSources(url, onLoad, onProgress, onError, options);
    } else {
      loadFromSingleSource(url, onLoad, onProgress, onError, options);
    }
  }

  this.loadFromWaitingList = () => {
    while (MAX_DOWNLOAD > concurrentDownloads) {
      const item = waitingList.shift();
      if (item) {
        this.load(item.url, item.onLoad, item.onProgress, item.onError, item.options);
      } else {
        return;
      }
    }
  }

  this.itemRemainingCheck = () => {
    if (waitingList.length === 0 && concurrentDownloads === 0) {
      for (let key in indexedLoaders) {
        if (indexedLoaders.hasOwnProperty(key)) {
          delete indexedLoaders[key];
        }
      }
    }
  }

  const onFinally = function(callback, loader, options) {
    return (...args) => {
      --concurrentDownloads;
      if (options?.msHandler) {
        options.msHandler.itemDownloaded(options.order, args);
      } else if (options?.isHandler) {
        options.isHandler.downloadCompleted(args);
      } else {
        if (callback) {
          callback(...args);
        }
      }
      loader.loadFromWaitingList();
      loader.itemRemainingCheck();
    }
  }

  this.parse = data => {
    return loader.parse(data);
  }

}