(function (factory) {

  if ( typeof define === 'function' && define.amd ) {

    // AMD. Register as an anonymous module.
    define([], factory);

  } else if ( typeof exports === 'object' ) {

    // Node/CommonJS
    module.exports = factory();

  } else {

    // Browser globals
    window.wNumb = factory();
  }

}(function(){

  'use strict';

  var FormatOptions = [
    'decimals',
    'thousand',
    'mark',
    'prefix',
    'suffix',
    'encoder',
    'decoder',
    'negativeBefore',
    'negative',
    'edit',
    'undo'
  ];

// General

  // Reverse a string
  function strReverse ( a ) {
    return a.split('').reverse().join('');
  }

  // Check if a string starts with a specified prefix.
  function strStartsWith ( input, match ) {
    return input.substring(0, match.length) === match;
  }

  // Check is a string ends in a specified suffix.
  function strEndsWith ( input, match ) {
    return input.slice(-1 * match.length) === match;
  }

  // Throw an error if formatting options are incompatible.
  function throwEqualError( F, a, b ) {
    if ( (F[a] || F[b]) && (F[a] === F[b]) ) {
      throw new Error(a);
    }
  }

  // Check if a number is finite and not NaN
  function isValidNumber ( input ) {
    return typeof input === 'number' && isFinite( input );
  }

  // Provide rounding-accurate toFixed method.
  // Borrowed: http://stackoverflow.com/a/21323330/775265
  function toFixed ( value, exp ) {
    value = value.toString().split('e');
    value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)));
    value = value.toString().split('e');
    return (+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp))).toFixed(exp);
  }


// Formatting

  // Accept a number as input, output formatted string.
  function formatTo ( decimals, thousand, mark, prefix, suffix, encoder, decoder, negativeBefore, negative, edit, undo, input ) {

    var originalInput = input, inputIsNegative, inputPieces, inputBase, inputDecimals = '', output = '';

    // Apply user encoder to the input.
    // Expected outcome: number.
    if ( encoder ) {
      input = encoder(input);
    }

    // Stop if no valid number was provided, the number is infinite or NaN.
    if ( !isValidNumber(input) ) {
      return false;
    }

    // Rounding away decimals might cause a value of -0
    // when using very small ranges. Remove those cases.
    if ( decimals !== false && parseFloat(input.toFixed(decimals)) === 0 ) {
      input = 0;
    }

    // Formatting is done on absolute numbers,
    // decorated by an optional negative symbol.
    if ( input < 0 ) {
      inputIsNegative = true;
      input = Math.abs(input);
    }

    // Reduce the number of decimals to the specified option.
    if ( decimals !== false ) {
      input = toFixed( input, decimals );
    }

    // Transform the number into a string, so it can be split.
    input = input.toString();

    // Break the number on the decimal separator.
    if ( input.indexOf('.') !== -1 ) {
      inputPieces = input.split('.');

      inputBase = inputPieces[0];

      if ( mark ) {
        inputDecimals = mark + inputPieces[1];
      }

    } else {

      // If it isn't split, the entire number will do.
      inputBase = input;
    }

    // Group numbers in sets of three.
    if ( thousand ) {
      inputBase = strReverse(inputBase).match(/.{1,3}/g);
      inputBase = strReverse(inputBase.join( strReverse( thousand ) ));
    }

    // If the number is negative, prefix with negation symbol.
    if ( inputIsNegative && negativeBefore ) {
      output += negativeBefore;
    }

    // Prefix the number
    if ( prefix ) {
      output += prefix;
    }

    // Normal negative option comes after the prefix. Defaults to '-'.
    if ( inputIsNegative && negative ) {
      output += negative;
    }

    // Append the actual number.
    output += inputBase;
    output += inputDecimals;

    // Apply the suffix.
    if ( suffix ) {
      output += suffix;
    }

    // Run the output through a user-specified post-formatter.
    if ( edit ) {
      output = edit ( output, originalInput );
    }

    // All done.
    return output;
  }

  // Accept a sting as input, output decoded number.
  function formatFrom ( decimals, thousand, mark, prefix, suffix, encoder, decoder, negativeBefore, negative, edit, undo, input ) {

    var originalInput = input, inputIsNegative, output = '';

    // User defined pre-decoder. Result must be a non empty string.
    if ( undo ) {
      input = undo(input);
    }

    // Test the input. Can't be empty.
    if ( !input || typeof input !== 'string' ) {
      return false;
    }

    // If the string starts with the negativeBefore value: remove it.
    // Remember is was there, the number is negative.
    if ( negativeBefore && strStartsWith(input, negativeBefore) ) {
      input = input.replace(negativeBefore, '');
      inputIsNegative = true;
    }

    // Repeat the same procedure for the prefix.
    if ( prefix && strStartsWith(input, prefix) ) {
      input = input.replace(prefix, '');
    }

    // And again for negative.
    if ( negative && strStartsWith(input, negative) ) {
      input = input.replace(negative, '');
      inputIsNegative = true;
    }

    // Remove the suffix.
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice
    if ( suffix && strEndsWith(input, suffix) ) {
      input = input.slice(0, -1 * suffix.length);
    }

    // Remove the thousand grouping.
    if ( thousand ) {
      input = input.split(thousand).join('');
    }

    // Set the decimal separator back to period.
    if ( mark ) {
      input = input.replace(mark, '.');
    }

    // Prepend the negative symbol.
    if ( inputIsNegative ) {
      output += '-';
    }

    // Add the number
    output += input;

    // Trim all non-numeric characters (allow '.' and '-');
    output = output.replace(/[^0-9\.\-.]/g, '');

    // The value contains no parse-able number.
    if ( output === '' ) {
      return false;
    }

    // Covert to number.
    output = Number(output);

    // Run the user-specified post-decoder.
    if ( decoder ) {
      output = decoder(output);
    }

    // Check is the output is valid, otherwise: return false.
    if ( !isValidNumber(output) ) {
      return false;
    }

    return output;
  }


// Framework

  // Validate formatting options
  function validate ( inputOptions ) {

    var i, optionName, optionValue,
      filteredOptions = {};

    if ( inputOptions['suffix'] === undefined ) {
      inputOptions['suffix'] = inputOptions['postfix'];
    }

    for ( i = 0; i < FormatOptions.length; i+=1 ) {

      optionName = FormatOptions[i];
      optionValue = inputOptions[optionName];

      if ( optionValue === undefined ) {

        // Only default if negativeBefore isn't set.
        if ( optionName === 'negative' && !filteredOptions.negativeBefore ) {
          filteredOptions[optionName] = '-';
          // Don't set a default for mark when 'thousand' is set.
        } else if ( optionName === 'mark' && filteredOptions.thousand !== '.' ) {
          filteredOptions[optionName] = '.';
        } else {
          filteredOptions[optionName] = false;
        }

        // Floating points in JS are stable up to 7 decimals.
      } else if ( optionName === 'decimals' ) {
        if ( optionValue >= 0 && optionValue < 8 ) {
          filteredOptions[optionName] = optionValue;
        } else {
          throw new Error(optionName);
        }

        // These options, when provided, must be functions.
      } else if ( optionName === 'encoder' || optionName === 'decoder' || optionName === 'edit' || optionName === 'undo' ) {
        if ( typeof optionValue === 'function' ) {
          filteredOptions[optionName] = optionValue;
        } else {
          throw new Error(optionName);
        }

        // Other options are strings.
      } else {

        if ( typeof optionValue === 'string' ) {
          filteredOptions[optionName] = optionValue;
        } else {
          throw new Error(optionName);
        }
      }
    }

    // Some values can't be extracted from a
    // string if certain combinations are present.
    throwEqualError(filteredOptions, 'mark', 'thousand');
    throwEqualError(filteredOptions, 'prefix', 'negative');
    throwEqualError(filteredOptions, 'prefix', 'negativeBefore');

    return filteredOptions;
  }

  // Pass all options as function arguments
  function passAll ( options, method, input ) {
    var i, args = [];

    // Add all options in order of FormatOptions
    for ( i = 0; i < FormatOptions.length; i+=1 ) {
      args.push(options[FormatOptions[i]]);
    }

    // Append the input, then call the method, presenting all
    // options as arguments.
    args.push(input);
    return method.apply('', args);
  }

  function wNumb ( options ) {

    if ( !(this instanceof wNumb) ) {
      return new wNumb ( options );
    }

    if ( typeof options !== "object" ) {
      return;
    }

    options = validate(options);

    // Call 'formatTo' with proper arguments.
    this.to = function ( input ) {
      return passAll(options, formatTo, input);
    };

    // Call 'formatFrom' with proper arguments.
    this.from = function ( input ) {
      return passAll(options, formatFrom, input);
    };
  }

  return wNumb;

}));
