const Parser = require('./Parser');
const path = require('path');
const {errorUtils} = require('@parcel/utils');

/**
 * A Pipeline composes multiple Asset types together.
 */
class Pipeline {
  constructor(options) {
    this.options = options;
    this.parser = new Parser(options);
  }

  async process(path, isWarmUp) {
    let options = this.options;
    if (isWarmUp) {
      options = Object.assign({isWarmUp}, options);
    }

    let asset = this.parser.getAsset(path, options);
    let error = null;
    let generatedMap = {};
    try {
      let generated = await this.processAsset(asset);
      for (let rendition of generated) {
        generatedMap[rendition.type] = rendition.value;
      }
    } catch (err) {
      error = errorUtils.errorToJson(err);
      error.fileName = path;
    }

    return {
      id: asset.id,
      dependencies: Array.from(asset.dependencies.values()),
      generated: generatedMap,
      sourceMaps: asset.sourceMaps,
      error: error,
      hash: asset.hash,
      cacheData: asset.cacheData,
    };
  }

  async processAsset(asset) {
    try {
      await asset.process();
    } catch (err) {
      throw asset.generateErrorMessage(err);
    }

    let inputType = path.extname(asset.name).slice(1);
    let generated = [];
    for (let rendition of this.iterateRenditions(asset)) {
      let {type, value} = rendition;
      if (typeof value !== 'string' || rendition.final) {
        generated.push(rendition);
        continue;
      }

      // Find an asset type for the rendition type.
      // If the asset is not already an instance of this asset type, process it.
      let AssetType = this.parser.findParser(
        asset.name.slice(0, -inputType.length) + type,
        true,
      );
      if (!(asset instanceof AssetType)) {
        let opts = Object.assign({}, asset.options, {rendition});
        let subAsset = new AssetType(asset.name, opts);
        subAsset.id = asset.id;
        subAsset.contents = value;
        subAsset.dependencies = asset.dependencies;
        subAsset.cacheData = Object.assign(asset.cacheData, subAsset.cacheData);

        let processed = await this.processAsset(subAsset);
        if (rendition.meta) {
          for (let res of processed) {
            res.meta = rendition.meta;
            res.isMain = res.type === subAsset.type;
          }
        }

        generated = generated.concat(processed);
      } else {
        generated.push(rendition);
      }
    }

    // Post process. This allows assets a chance to modify the output produced by sub-asset types.
    try {
      generated = await asset.postProcess(generated);
    } catch (err) {
      throw asset.generateErrorMessage(err);
    }

    let hasMap = false;
    let sourceMaps = {};
    for (let rendition of generated) {
      if (rendition.map && rendition.type == asset.type) {
        sourceMaps[rendition.type] = rendition.map;
        hasMap = true;
      }
    }

    if (hasMap) {
      asset.sourceMaps = sourceMaps;
    }

    asset.generated = generated;
    asset.hash = await asset.generateHash();

    return generated;
  }

  *iterateRenditions(asset) {
    if (Array.isArray(asset.generated)) {
      return yield* asset.generated;
    }

    if (typeof asset.generated === 'string') {
      return yield {
        type: asset.type,
        value: asset.generated,
      };
    }

    // Backward compatibility support for the old API.
    // Assume all renditions are final - don't compose asset types together.
    for (let type in asset.generated) {
      yield {
        type,
        value: asset.generated[type],
        // for scope hoisting, we need to post process all JS
        final: !(type === 'js' && this.options.scopeHoist),
      };
    }
  }
}

module.exports = Pipeline;
