'use strict';

var assign    = require('lodash.assign'),
    defaults  = require('lodash.defaults'),
    camelcase = require('camelcase');

var debugMessage      = require('./debug-message'),
    toRegExp          = require('./to-reg-exp'),
    throwErrors       = require('./throw-errors'),
    decodeSourcesWith = require('./decode-sources-with'),
    locateRootWith    = require('./locate-root-with'),
    encodeSourcesWith = require('./encode-sources-with'),
    testCodec         = require('./test-codec');

const CODECS = require('../../codec');

/**
 * Process the given source-map per the given options.
 * @param {{resourcePath:string, context:string, output:{path:string}}} context A loader or compilation
 * @param {{debug:boolean, fail:boolean, format:string|boolean, root:string, codecs:object}} options Options hash
 * @param {object|string} sourceMapOrSource An incoming source-map or single source path
 * @returns {undefined|object|string} An amended source-map or source path else undefined
 */
function process(context, options, sourceMapOrSource) {

  // default options
  defaults(options, {
    sep   : '/',
    debug : false,
    fail  : false,
    format: false,
    root  : false,
    codecs: CODECS
  });

  // validate codecs
  var codecs = options.codecs
    .filter(testCodec);

  // determine what is present
  var inputMap     = !!sourceMapOrSource && (typeof sourceMapOrSource === 'object') && sourceMapOrSource,
      inputPath    = (typeof sourceMapOrSource === 'string') && sourceMapOrSource,
      inputSources = inputMap && inputMap.sources || inputPath && [inputPath];

  // what we need to produce
  var absoluteSources,
      outputSources,
      outputRoot,
      outputMap;

  if (inputSources) {

    // decode each source with the first valid codec
    absoluteSources = inputSources
      .map(decodeSourcesWith.call(context, codecs, options.fail));

    // check for decode errors
    throwErrors(context.resourcePath, absoluteSources);

    // output map is a copy unless absent or we are removing
    outputMap = (!inputMap || (options.format === 'remove')) ? undefined : assign({}, inputMap);

    // some change in format
    if (options.format) {

      // find the specified codec in the codecs list
      var codec = codecs
        .filter(testNamedCodec)
        .pop();

      if (!codec) {
        throw new Error('Specified format "' + options.format + '" does not match any available codec.');
      }

      // use the encoder where specified in 'format'
      outputSources = absoluteSources
        .map(encodeSourcesWith.call(context, codec))
        .map(insertAbstractSources)
        .map(convertPathSep);

      outputRoot = !!options.root && locateRootWith.call(context, codec)() || undefined;

      // check for encode errors
      throwErrors(context.resourcePath, outputSources.concat(outputRoot));

      // commit the change
      if (outputMap) {
        outputMap.sources = outputSources;
        outputMap.sourceRoot = outputRoot;
      }
    }
  }

  // debugging information
  var isDebug = toRegExp(options.debug).test(context.resourcePath);
  if (isDebug) {
    console.log(debugMessage(context, {
      input   : inputSources,
      absolute: absoluteSources,
      output  : outputSources,
      root    : outputRoot
    }));
  }

  // complete
  return inputMap ? outputMap : outputSources ? outputSources[0] : undefined;

  function testNamedCodec(value) {
    return (camelcase(value.name) === options.format);
  }

  function insertAbstractSources(value, i) {
    return value || inputSources[i];
  }

  function convertPathSep(value) {
    return (value instanceof Error) ? value : value.replace(/[\\\/]/g, options.sep);
  }
}

module.exports = process;