Skip to content
Snippets Groups Projects
index.js 7.88 MiB
Newer Older
Romain CREY's avatar
Romain CREY committed
/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

const {
  BaseKonnector,
  log,
  errors,
  addData,
  hydrateAndFilter,
  cozyClient
} = __webpack_require__(1)

const fetch = __webpack_require__(1310)
const moment = __webpack_require__(1319)
__webpack_require__(1457)

moment.locale('fr') // set the language
moment.tz.setDefault('Europe/Paris') // set the timezone

const manualExecution =
  process.env.COZY_JOB_MANUAL_EXECUTION === 'true' ? true : false

const startDate = manualExecution
  ? moment()
      .subtract(1, 'year')
      .format('MM/DD/YYYY')
  : moment()
      .subtract(3, 'year')
      .format('MM/DD/YYYY')

const endDate = moment().format('MM/DD/YYYY')
// const timeRange = ['day', 'month', 'year']
const rangeDate = {
  day: {
    doctype: 'com.grandlyon.egl.day',
    keys: ['year', 'month', 'day']
    doctype: 'com.grandlyon.egl.month',
    keys: ['year', 'month']
    doctype: 'com.grandlyon.egl.year',
    keys: ['year']
Romain CREY's avatar
Romain CREY committed

module.exports = new BaseKonnector(start)
Romain CREY's avatar
Romain CREY committed

// The start function is run by the BaseKonnector instance only when it got all the account
// information (fields). When you run this connector yourself in "standalone" mode or "dev" mode,
// the account information come from ./konnector-dev-config.json file
async function start(fields, cozyParameters) {
  try {
    // Local debug data
    // const baseUrl = fields.eglBaseURL
    // const apiAuthKey = fields.eglAPIAuthKey
    const baseUrl = cozyParameters.secret.eglBaseURL
    const apiAuthKey = cozyParameters.secret.eglAPIAuthKey
    log('info', 'Authenticating ...')
Romain CREY's avatar
Romain CREY committed
    const response = await authenticate(
      fields.login,
      fields.password,
      baseUrl,
      apiAuthKey
    log('info', 'Successfully logged in')

    const eglData = await getData(response, baseUrl, apiAuthKey)
    if (eglData) {
      log('debug', 'Process egl daily data')
      const processedLoadData = await processData(
        eglData,
        rangeDate.day.doctype,
        rangeDate.day.keys
      log('debug', 'Agregate egl load data for month and year')
      await agregateMonthAndYearData(processedLoadData)
    } else {
      log('debug', 'No data found')
Romain CREY's avatar
Romain CREY committed
  } catch (error) {
    throw new Error(error.message)
Romain CREY's avatar
Romain CREY committed
  }
}
/**
 * Parse data
 * Remove existing data from DB using hydrateAndFilter
 * Store filtered data
 * Return the list of filtered data
 */
async function processData(data, doctype, filterKeys) {
  // const formatedData = await formateData(data)
  log('debug', 'processData - data formated')
  // Remove data for existing days into the DB
  const filteredData = await hydrateAndFilter(data, doctype, {
    keys: filterKeys
  })
  log('debug', 'processData - data filtered')
  // Store new day data
  await storeData(filteredData, doctype, filterKeys)
  return filteredData
}

/**
 * Agregate data from daily data to monthly and yearly data
 */
async function agregateMonthAndYearData(data) {
  // Sum year and month values into object with year or year-month as keys
  if (data && data.length !== 0) {
    let monthData = {}
    let yearData = {}

    data.forEach(element => {
      element.year + '-' + element.month in monthData
        ? (monthData[element.year + '-' + element.month] += element.load)
        : (monthData[element.year + '-' + element.month] = element.load)
      element.year in yearData
        ? (yearData[element.year] += element.load)
        : (yearData[element.year] = element.load)
    })
    // Agregation for Month data
    const agregatedMonthData = await buildAgregatedData(
      monthData,
      'com.grandlyon.egl.month'
    )
    await storeData(agregatedMonthData, 'com.grandlyon.egl.month', [
      'year',
      'month'
    ])
    // Agregation for Year data
    const agregatedYearData = await buildAgregatedData(
      yearData,
      'com.grandlyon.egl.year'
    )
    await storeData(agregatedYearData, 'com.grandlyon.egl.year', ['year'])
  }
}

/**
 * Retrieve and remove old data for a specific doctype
 * Return an Array of agregated data
 */
async function buildAgregatedData(data, doctype) {
  log('info', 'entering buildAgregatedData')
  let agregatedData = []
  for (let [key, value] of Object.entries(data)) {
    const data = await buildDataFromKey(doctype, key, value)
    const oldValue = await resetInProgressAggregatedData(data, doctype)
    log('info', 'Dataload + oldvalue is ' + data.load + ' + ' + oldValue)
    data.load += oldValue
    agregatedData.push(data)
  }
  return agregatedData
}

Romain CREY's avatar
Romain CREY committed
async function authenticate(login, password, baseUrl, apiAuthKey) {
  const authRequest = {
Romain CREY's avatar
Romain CREY committed
    headers: {
      AuthKey: apiAuthKey,
      "Content-Type": "application/x-www-form-urlencoded"
Romain CREY's avatar
Romain CREY committed
    },
    body: new URLSearchParams({
Romain CREY's avatar
Romain CREY committed
      login: login,
      pass: password
  const data = await fetch(baseUrl + "/connect.aspx", authRequest)
  const response = await data.json()

Yoan VALLET's avatar
Yoan VALLET committed
  if (response.codeRetour === 100) {
    return response
Yoan VALLET's avatar
Yoan VALLET committed
  } else {
    throw new Error(errors.LOGIN_FAILED)
Romain CREY's avatar
Romain CREY committed
  }
}

async function getData(response, baseUrl, apiAuthKey) {
  log('debug', 'Start date : ' + startDate)
  log('debug', 'End date : ' + endDate)
Romain CREY's avatar
Romain CREY committed
  const dataRequest = {
Romain CREY's avatar
Romain CREY committed
    headers: {
      AuthKey: apiAuthKey,
      'Content-Type': 'application/x-www-form-urlencoded'
Romain CREY's avatar
Romain CREY committed
    },
    body: new URLSearchParams({
Romain CREY's avatar
Romain CREY committed
      token: response.resultatRetour.token,
      num_abt: response.resultatRetour.num_abt,
      date_debut: startDate,
      date_fin: endDate
Romain CREY's avatar
Romain CREY committed
  try {
    // Sort data by date
    const data = await fetch(
      baseUrl + "/getAllAgregatsByAbonnement.aspx",
      dataRequest
    )
    const responseEgl = await data.json()
    responseEgl.resultatRetour.sort(function(a, b) {
      return new Date(a.DateReleve) - new Date(b.DateReleve);
Yoan VALLET's avatar
Yoan VALLET committed
    switch (responseEgl.codeRetour) {
Romain CREY's avatar
Romain CREY committed
      case 100:
        return format(responseEgl)
Romain CREY's avatar
Romain CREY committed
      case -2:
        throw errors.LOGIN_FAILED
Romain CREY's avatar
Romain CREY committed
      case -1:
        throw errors.VENDOR_DOWN
Romain CREY's avatar
Romain CREY committed
      default:
        throw errors.UNKNOWN_ERROR
Romain CREY's avatar
Romain CREY committed
    }
  } catch (error) {
    log('debug', 'Error from getAllAgregatsByAbonnement')
    throw new Error(errors.VENDOR_DOWN)
Romain CREY's avatar
Romain CREY committed
  }
}

function format(response) {
  log('info', 'origin response size is : ' + response.resultatRetour.length)
  // Store first value as reference for index processing
  let refValue = response.resultatRetour[0]
  // Create copy of data without first value
Yoan VALLET's avatar
Yoan VALLET committed
  const data = response.resultatRetour
    .slice(1)
    .filter(value => value.ValeurIndex)
  log('info', 'filtered size is : ' + data.length)
  return data.map(value => {
    const time = moment(value.DateReleve, moment.ISO_8601)
    const procesedLoad = value.ValeurIndex - refValue.ValeurIndex
    if (procesedLoad < 0) {
      log(
        'error',
        `processing load for day ${parseInt(time.format('D'))}/${parseInt(
          time.format('M')
        )}/${parseInt(time.format('YYYY'))}, value is : ${procesedLoad}`
      )
      throw errors.VENDOR_DOWN
    }
    // Change index ref value
    refValue = value
    return {
      load: procesedLoad,
      year: parseInt(time.format('YYYY')),
      month: parseInt(time.format('M')),
      day: parseInt(time.format('D')),
      hour: 0,
      minute: 0,
      type: value.TypeAgregat
Yoan VALLET's avatar
Yoan VALLET committed
    }

/**
 * Save data in the right doctype db and prevent duplicated keys
 */
async function storeData(data, doctype, filterKeys) {
  log('debug', 'Store into ' + doctype)
  log('debug', 'Store into keys : ' + filterKeys)
  // data.map(v => {
  //   log("info", "Saving data " + v.load + " for " + v.day + "/" + v.month + "/" + v.year);
  // });
  const filteredDocuments = await hydrateAndFilter(data, doctype, {
    keys: filterKeys
  })
  return await addData(filteredDocuments, doctype)
}

/**
 * Format an entry for DB storage
 * using key and value
 * For year doctype: key = "YYYY"
 * For month doctype: key = "YYYY-MM"
 */
async function buildDataFromKey(doctype, key, value) {
  let year, month, day, hour
  if (doctype === 'com.grandlyon.egl.year') {
    year = key
    month = 1
    day = 0
    hour = 0
  } else if (doctype === 'com.grandlyon.egl.month') {
    const split = key.split('-')
    year = split[0]
    month = split[1]
    day = 0
    hour = 0
  } else {
    const split = key.split('-')
    year = split[0]
    month = split[1]
    day = split[2]
    hour = split[3]
  return {
    load: Math.round(value * 10000) / 10000,
    year: parseInt(year),
    month: parseInt(month),
    day: parseInt(day),
    hour: parseInt(hour),
    minute: 0
/**
 * Function handling special case.
 * The temporary aggregated data need to be remove in order for the most recent one te be saved.
Yoan VALLET's avatar
Yoan VALLET committed
 * ex for com.grandlyon.egl.month :
 * { load: 76.712, month: 2020, ... } need to be replace by
 * { load: 82.212, month: 2020, ... } after egl data reprocess
async function resetInProgressAggregatedData(data, doctype) {
  // /!\ Warning: cannot use mongo queries because not supported for dev by cozy-konnectors-libs
  log('debug', 'Remove aggregated data for ' + doctype)
  const result = await cozyClient.data.findAll(doctype)
  if (result && result.length > 0) {
    // Filter data to remove
    var filtered = []
    if (doctype === 'com.grandlyon.egl.year') {
      // Yearly case
      filtered = result.filter(function(el) {
        return el.year == data.year
      })
    } else if (doctype === 'com.grandlyon.egl.month') {
      filtered = result.filter(function(el) {
        return el.year == data.year && el.month == data.month
      })
    } else {
      // Hourly case
      filtered = result.filter(function(el) {
        return (
          el.year == data.year &&
          el.month == data.month &&
          el.day == data.day &&
          el.hour == data.hour
        )
      })
    }
    // Remove data
    let sum = 0.0
    for (const doc of filtered) {
      sum += doc.load
      log('debug', 'Removing this entry for ' + doc.load)
      await cozyClient.data.delete(doctype, doc)
    return sum
Romain CREY's avatar
Romain CREY committed
  }
  return 0.0
Romain CREY's avatar
Romain CREY committed
}


/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {

const log = __webpack_require__(2).namespace('cozy-konnector-libs');
Romain CREY's avatar
Romain CREY committed

const requestFactory = __webpack_require__(22);
Romain CREY's avatar
Romain CREY committed

const hydrateAndFilter = __webpack_require__(369);
Romain CREY's avatar
Romain CREY committed

const categorization = __webpack_require__(1165);
Romain CREY's avatar
Romain CREY committed

module.exports = {
  BaseKonnector: __webpack_require__(1225),
  CookieKonnector: __webpack_require__(1303),
  cozyClient: __webpack_require__(485),
  errors: __webpack_require__(1231),
Romain CREY's avatar
Romain CREY committed
  log,
  saveFiles: __webpack_require__(1227),
  saveBills: __webpack_require__(1226),
  saveIdentity: __webpack_require__(1269),
  linkBankOperations: __webpack_require__(1248),
  addData: __webpack_require__(1247),
Romain CREY's avatar
Romain CREY committed
  hydrateAndFilter,
  htmlToPDF: __webpack_require__(1304).htmlToPDF,
  createCozyPDFDocument: __webpack_require__(1304).createCozyPDFDocument,
Romain CREY's avatar
Romain CREY committed
  filterData: deprecate(hydrateAndFilter, 'Use hydrateAndFilter now. filterData will be removed in cozy-konnector-libs@4'),
  updateOrCreate: __webpack_require__(1268),
Romain CREY's avatar
Romain CREY committed
  request: deprecate(requestFactory, 'Use requestFactory instead of request. It will be removed in cozy-konnector-libs@4'),
  requestFactory,
  retry: __webpack_require__(1228),
  wrapIfSentrySetUp: __webpack_require__(1270).wrapIfSentrySetUp,
  Document: __webpack_require__(1305),
  signin: __webpack_require__(1265),
  submitForm: __webpack_require__(1265),
  scrape: __webpack_require__(1307),
  mkdirp: __webpack_require__(1230),
  normalizeFilename: __webpack_require__(1308),
  utils: __webpack_require__(484),
  solveCaptcha: __webpack_require__(1309),
Romain CREY's avatar
Romain CREY committed
  createCategorizer: categorization.createCategorizer,
  categorize: categorization.categorize,
  manifest: __webpack_require__(836)
Romain CREY's avatar
Romain CREY committed
};

function deprecate(wrapped, message) {
  return function () {
    log('warn', message);
    return wrapped.apply(this, arguments);
  };
}

/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {

const { filterLevel, filterSecrets } = __webpack_require__(3)
const Secret = __webpack_require__(4)
const { LOG_LEVEL } = process.env
Romain CREY's avatar
Romain CREY committed
let level = LOG_LEVEL || 'debug'
const format = __webpack_require__(5)
Romain CREY's avatar
Romain CREY committed
const filters = [filterLevel, filterSecrets]

const filterOut = function() {
  for (const filter of filters) {
    if (filter.apply(null, arguments) === false) {
      return true
    }
  }
  return false
}

/**
 * Use it to log messages in your konnector. Typical types are
 *
 * - `debug`
 * - `warning`
 * - `info`
 * - `error`
 * - `ok`
 *
 *
 * @example
 *
 * They will be colored in development mode. In production mode, those logs are formatted in JSON to be interpreted by the stack and possibly sent to the client. `error` will stop the konnector.
 *
 * ```js
 * logger = log('my-namespace')
 * logger('debug', '365 bills')
 * // my-namespace : debug : 365 bills
 * logger('info', 'Page fetched')
 * // my-namespace : info : Page fetched
 * ```
 * @param  {string} type
 * @param  {string} message
 * @param  {string} label
 * @param  {string} namespace
 */
function log(type, message, label, namespace) {
  if (filterOut(level, type, message, label, namespace)) {
    return
  }
  // eslint-disable-next-line no-console
  console.log(format(type, message, label, namespace))
}

log.addFilter = function(filter) {
  return filters.push(filter)
}

log.setLevel = function(lvl) {
  level = lvl
}

// Short-hands
const methods = ['debug', 'info', 'warn', 'error', 'ok', 'critical']
methods.forEach(level => {
  log[level] = function(message, label, namespace) {
    return log(level, message, label, namespace)
  }
})

module.exports = log

log.setNoRetry = obj => {
  if (obj) obj.no_retry = true
  else obj = { no_retry: true }
  return obj.no_retry
}
log.Secret = Secret
log.namespace = function(namespace) {
  return function(type, message, label, ns = namespace) {
    log(type, message, label, ns)
  }
}


/***/ }),
/* 3 */
/***/ (function(module, exports, __webpack_require__) {

const levels = {
  secret: 0,
  debug: 10,
  info: 20,
  warn: 30,
  error: 40,
  ok: 50,
  critical: 50
const Secret = __webpack_require__(4)
const filterSecrets = function(level, type, message) {
  if (type !== 'secret' && message instanceof Secret) {
    throw new Error('You should log a secret with log.secret')
  }
}

const filterLevel = function(level, type) {
  return levels[type] >= levels[level]
}

module.exports = {
  filterSecrets,
  filterLevel
}


/***/ }),
/* 4 */
/***/ (function(module, exports) {

const Secret = function(data) {
  Object.assign(this, data)
  return this
Romain CREY's avatar
Romain CREY committed
}

Secret.prototype.toString = function() {
  throw new Error('Cannot convert Secret to string')
}

module.exports = Secret


/***/ }),
/* 5 */
/***/ (function(module, exports, __webpack_require__) {

const prodFormat = __webpack_require__(6)
const devFormat = __webpack_require__(8)

switch ("none") {
  case 'production':
    module.exports = prodFormat
    break
  case 'development':
    module.exports = devFormat
    break
  case 'standalone':
    module.exports = devFormat
    break
  case 'test':
    module.exports = devFormat
    break
  default:
    module.exports = prodFormat
}


/***/ }),
/* 6 */
/***/ (function(module, exports, __webpack_require__) {

const stringify = __webpack_require__(7)

const LOG_LENGTH_LIMIT = 64 * 1024 - 1

Romain CREY's avatar
Romain CREY committed
function prodFormat(type, message, label, namespace) {
  const log = { time: new Date(), type, label, namespace }

  if (typeof message === 'object') {
    if (message && message.no_retry) {
      log.no_retry = message.no_retry
    }
    if (message && message.message) {
      log.message = message.message
    }
  } else {
    log.message = message
  }

  // properly display error messages
  if (log.message && log.message.stack) {
    log.message = log.message.stack
  }

  // cut the string to avoid a fail in the stack
  let result = log
  try {
    result = stringify(log).substr(0, LOG_LENGTH_LIMIT)
  } catch (err) {
    // eslint-disable-next-line no-console
    console.log(err.message, 'cozy-logger: Failed to convert message to JSON')
  }
  return result
}

module.exports = prodFormat


/***/ }),
/* 7 */
/***/ (function(module, exports) {

exports = module.exports = stringify
exports.getSerialize = serializer

function stringify(obj, replacer, spaces, cycleReplacer) {
  return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces)
}

function serializer(replacer, cycleReplacer) {
  var stack = [], keys = []

  if (cycleReplacer == null) cycleReplacer = function(key, value) {
    if (stack[0] === value) return "[Circular ~]"
    return "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]"
  }

  return function(key, value) {
    if (stack.length > 0) {
      var thisPos = stack.indexOf(this)
      ~thisPos ? stack.splice(thisPos + 1) : stack.push(this)
      ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key)
      if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value)
    }
    else stack.push(value)

    return replacer == null ? value : replacer.call(this, key, value)
  }
}


/***/ }),
/* 8 */
/***/ (function(module, exports, __webpack_require__) {

const util = __webpack_require__(9)
const chalk = __webpack_require__(10)

if (util && util.inspect && util.inspect.defaultOptions) {
  util.inspect.defaultOptions.maxArrayLength = null
  util.inspect.defaultOptions.depth = 2
  util.inspect.defaultOptions.colors = true
}

const type2color = {
  debug: 'cyan',
  warn: 'yellow',
  info: 'blue',
  error: 'red',
  ok: 'green',
  secret: 'red',
  critical: 'red'
}

Romain CREY's avatar
Romain CREY committed
function devFormat(type, message, label, namespace) {
  let formatmessage = message

  if (typeof formatmessage !== 'string') {
    formatmessage = util.inspect(formatmessage)
  }

  let formatlabel = label ? ` : "${label}" ` : ''
  let formatnamespace = namespace ? chalk.magenta(`${namespace}: `) : ''

  let color = type2color[type]
  let formattype = color ? chalk[color](type) : type

  return `${formatnamespace}${formattype}${formatlabel} : ${formatmessage}`
}

module.exports = devFormat
Romain CREY's avatar
Romain CREY committed


/***/ }),
/* 9 */
/***/ (function(module, exports) {

module.exports = require("util");
Romain CREY's avatar
Romain CREY committed

/***/ }),
/* 10 */
Romain CREY's avatar
Romain CREY committed
/***/ (function(module, exports, __webpack_require__) {

"use strict";

const escapeStringRegexp = __webpack_require__(11);
const ansiStyles = __webpack_require__(12);
const stdoutColor = __webpack_require__(18).stdout;
Romain CREY's avatar
Romain CREY committed

const template = __webpack_require__(21);
Romain CREY's avatar
Romain CREY committed

const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm');

// `supportsColor.level` → `ansiStyles.color[name]` mapping
const levelMapping = ['ansi', 'ansi', 'ansi256', 'ansi16m'];

// `color-convert` models to exclude from the Chalk API due to conflicts and such
const skipModels = new Set(['gray']);

const styles = Object.create(null);

function applyOptions(obj, options) {
	options = options || {};

	// Detect level if not set manually
	const scLevel = stdoutColor ? stdoutColor.level : 0;
	obj.level = options.level === undefined ? scLevel : options.level;
	obj.enabled = 'enabled' in options ? options.enabled : obj.level > 0;
}

function Chalk(options) {
	// We check for this.template here since calling `chalk.constructor()`
	// by itself will have a `this` of a previously constructed chalk object
	if (!this || !(this instanceof Chalk) || this.template) {
		const chalk = {};
		applyOptions(chalk, options);

		chalk.template = function () {
			const args = [].slice.call(arguments);
			return chalkTag.apply(null, [chalk.template].concat(args));
		};

		Object.setPrototypeOf(chalk, Chalk.prototype);
		Object.setPrototypeOf(chalk.template, chalk);

		chalk.template.constructor = Chalk;

		return chalk.template;
	}

	applyOptions(this, options);
}

// Use bright blue on Windows as the normal blue color is illegible
if (isSimpleWindowsTerm) {
	ansiStyles.blue.open = '\u001B[94m';
}

for (const key of Object.keys(ansiStyles)) {
	ansiStyles[key].closeRe = new RegExp(escapeStringRegexp(ansiStyles[key].close), 'g');

	styles[key] = {
		get() {
			const codes = ansiStyles[key];
			return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, key);
		}
	};
}

styles.visible = {
	get() {
		return build.call(this, this._styles || [], true, 'visible');
	}
};

ansiStyles.color.closeRe = new RegExp(escapeStringRegexp(ansiStyles.color.close), 'g');
for (const model of Object.keys(ansiStyles.color.ansi)) {
	if (skipModels.has(model)) {
		continue;
	}

	styles[model] = {
		get() {
			const level = this.level;
			return function () {
				const open = ansiStyles.color[levelMapping[level]][model].apply(null, arguments);
				const codes = {
					open,
					close: ansiStyles.color.close,
					closeRe: ansiStyles.color.closeRe
				};
				return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, model);
			};
		}
	};
}

ansiStyles.bgColor.closeRe = new RegExp(escapeStringRegexp(ansiStyles.bgColor.close), 'g');
for (const model of Object.keys(ansiStyles.bgColor.ansi)) {
	if (skipModels.has(model)) {
		continue;
	}

	const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1);
	styles[bgModel] = {
		get() {
			const level = this.level;
			return function () {
				const open = ansiStyles.bgColor[levelMapping[level]][model].apply(null, arguments);
				const codes = {
					open,
					close: ansiStyles.bgColor.close,
					closeRe: ansiStyles.bgColor.closeRe
				};
				return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, model);
			};
		}
	};
}

const proto = Object.defineProperties(() => {}, styles);

function build(_styles, _empty, key) {
	const builder = function () {
		return applyStyle.apply(builder, arguments);
	};

	builder._styles = _styles;
	builder._empty = _empty;

	const self = this;

	Object.defineProperty(builder, 'level', {
		enumerable: true,
		get() {
			return self.level;
		},
		set(level) {
			self.level = level;
		}
	});

	Object.defineProperty(builder, 'enabled', {
		enumerable: true,
		get() {
			return self.enabled;
		},
		set(enabled) {
			self.enabled = enabled;
		}
	});

	// See below for fix regarding invisible grey/dim combination on Windows
	builder.hasGrey = this.hasGrey || key === 'gray' || key === 'grey';

	// `__proto__` is used because we must return a function, but there is
	// no way to create a function with a different prototype
	builder.__proto__ = proto; // eslint-disable-line no-proto

	return builder;
}

function applyStyle() {
	// Support varags, but simply cast to string in case there's only one arg
	const args = arguments;
	const argsLen = args.length;
	let str = String(arguments[0]);

	if (argsLen === 0) {
		return '';
	}

	if (argsLen > 1) {
		// Don't slice `arguments`, it prevents V8 optimizations
		for (let a = 1; a < argsLen; a++) {
			str += ' ' + args[a];
		}
	}

	if (!this.enabled || this.level <= 0 || !str) {
		return this._empty ? '' : str;
	}

	// Turns out that on Windows dimmed gray text becomes invisible in cmd.exe,
	// see https://github.com/chalk/chalk/issues/58
	// If we're on Windows and we're dealing with a gray color, temporarily make 'dim' a noop.
	const originalDim = ansiStyles.dim.open;
	if (isSimpleWindowsTerm && this.hasGrey) {
		ansiStyles.dim.open = '';
	}

	for (const code of this._styles.slice().reverse()) {
		// Replace any instances already present with a re-opening code
		// otherwise only the part of the string until said closing code
		// will be colored, and the rest will simply be 'plain'.
		str = code.open + str.replace(code.closeRe, code.open) + code.close;

		// Close the styling before a linebreak and reopen
		// after next line to fix a bleed issue on macOS
		// https://github.com/chalk/chalk/pull/92
		str = str.replace(/\r?\n/g, `${code.close}$&${code.open}`);
	}

	// Reset the original `dim` if we changed it to work around the Windows dimmed gray issue
	ansiStyles.dim.open = originalDim;

	return str;
}

function chalkTag(chalk, strings) {
	if (!Array.isArray(strings)) {
		// If chalk() was called by itself or with a string,
		// return the string itself as a string.
		return [].slice.call(arguments, 1).join(' ');
	}

	const args = [].slice.call(arguments, 2);
	const parts = [strings.raw[0]];

	for (let i = 1; i < strings.length; i++) {
		parts.push(String(args[i - 1]).replace(/[{}\\]/g, '\\$&'));
		parts.push(String(strings.raw[i]));
	}

	return template(chalk, parts.join(''));