"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.isMessageWithUsageModel = isMessageWithUsageModel;
exports.isMessageWithCode = isMessageWithCode;
exports.isUsageError = isUsageError;
exports.default = exports.UsageError = void 0;

var _debug = _interopRequireDefault(require("debug"));

var _capabilities = require("./capabilities");

var _types = require("../util/types");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/*
 * Copyright 2017-2019 IBM Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) {
  function adopt(value) {
    return value instanceof P ? value : new P(function (resolve) {
      resolve(value);
    });
  }

  return new (P || (P = Promise))(function (resolve, reject) {
    function fulfilled(value) {
      try {
        step(generator.next(value));
      } catch (e) {
        reject(e);
      }
    }

    function rejected(value) {
      try {
        step(generator["throw"](value));
      } catch (e) {
        reject(e);
      }
    }

    function step(result) {
      result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
    }

    step((generator = generator.apply(thisArg, _arguments || [])).next());
  });
};
/* eslint-disable @typescript-eslint/explicit-member-accessibility */


const debug = (0, _debug.default)('core/usage-error');
debug('loading');

class DefaultUsageOptions {}
/** lazily load repl */


function pexec(command) {
  return () => __awaiter(this, void 0, void 0, function* () {
    const {
      pexec
    } = yield Promise.resolve().then(() => require('../repl/exec'));
    pexec(command);
  });
}
/**
 * Map a asynchronous function to an array sequentially from front to
 * back.
 *
 */


function promiseEach(arr, fn) {
  return __awaiter(this, void 0, void 0, function* () {
    const result = [];

    for (const item of arr) {
      result.push(yield fn(item));
    }

    return result;
  });
}
/** Create an HTML DIV to wrap around the given string */


const div = (str, css = undefined, tag = 'div') => {
  const result = document.createElement(tag);

  if (str) {
    if (typeof str === 'string') {
      Promise.resolve(str).then(str => {
        result.innerText = str;
      });
    } else {
      result.appendChild(str);
    }
  }

  if (css) {
    if (typeof css === 'string') {
      result.className = css;
    } else {
      css.forEach(_ => result.classList.add(_));
    }
  }

  return result;
};

const span = (str, css) => div(str, css, 'span');
/**
 * The start of every section, e.g. Usage:
 *
 */


const prefix = str => {
  const prefix = div(str, 'usage-error-title', 'h4');
  prefix.setAttribute('data-title', str);
  return prefix;
};
/** A part of the main body of the usage message */


const bodyPart = (noMargin = false) => {
  const result = div();
  if (!noMargin) result.style.margin = '2em 0em 0 0';
  return result;
};
/** render the given div with the default san serif font */


const sans = div => {
  div.classList.add('sans-serif');
  return div;
};
/** render the given div a bit smaller */


const smaller = div => {
  div.classList.add('somewhat-smaller-text');
  return div;
};
/** render the given div with white space line wrapping */


const wrap = div => {
  div.style.display = 'block';
  div.style.whiteSpace = 'normal';
  return div;
};

function isBreadcrumbWithClickCommand(crumb) {
  const breadcrumb = crumb;
  return !!(breadcrumb.label && breadcrumb.command);
}

function isMessageWithUsageModel(msg) {
  const message = msg;
  return !!message.usage;
}
/**
 * Invoke a given command, and extract the breadcrumb title from the resulting usage model
 *
 */


const breadcrumbFromCommand = command => __awaiter(void 0, void 0, void 0, function* () {
  const {
    qexec
  } = yield Promise.resolve().then(() => require('../repl/exec'));
  const usageError = yield qexec(command, undefined, undefined, {
    failWithUsage: true
  });

  if (isMessageWithUsageModel(usageError.raw)) {
    const usage = usageError.raw.usage;
    return usage.breadcrumb || usage.title;
  } else {
    return usageError.message;
  }
});

const makeBreadcrumb = options => {
  let cmd;
  let label;

  if (typeof options.breadcrumb === 'string') {
    cmd = label = options.breadcrumb;
  } else if (isBreadcrumbWithClickCommand(options.breadcrumb)) {
    cmd = options.breadcrumb.command;
    label = options.breadcrumb.label;
  } else {
    cmd = options.breadcrumb.command;
    label = breadcrumbFromCommand(cmd);
  }

  return Promise.resolve(label).then(label => {
    const item = span();
    item.classList.add('bx--breadcrumb-item');
    const dom = span(label, 'bx--no-link');
    dom.setAttribute('data-label', label);
    item.appendChild(dom);

    if (!options.noSlash) {
      item.appendChild(span((0, _capabilities.isHeadless)() ? '/' : '', 'bx--breadcrumb-item--slash'));
    }

    if (cmd) {
      dom.classList.add('bx--link');
      dom.onclick = pexec(cmd);
    }

    return item;
  });
};

function isGenerator(row) {
  return !!row.fn;
}
/**
 * Format the given usage message
 *
 */


const format = (message, options = new DefaultUsageOptions()) => __awaiter(void 0, void 0, void 0, function* () {
  debug('format message', message);

  if (typeof message === 'string') {
    // then the content is already formatted
    return Promise.resolve(span(message));
  } else if ((0, _types.isHTML)(message)) {
    // same...
    return Promise.resolve(message);
    /* } else if (isGenerator<UsageLike>(message)) {
    // then message.fn is a generator, e.g. for command aliases
    return format(message.fn(message.command), undefined, options) */
  } else {
    // these are the fields of the usage message
    const usage = message.usage || {};
    const {
      command,
      docs,
      title,
      breadcrumb = title || command,
      header = docs && `${docs}.`,
      example,
      detailedExample,
      sampleInputs,
      intro,
      sections,
      // the more general case: the usage model has custom sections
      commandPrefix,
      commandPrefixNotNeeded,
      commandSuffix = '',
      preserveCase = false,
      // in breadcrumbs
      available,
      related,
      required,
      optional,
      oneof
    } = usage;
    const outerCommandPrefix = commandPrefix;
    const outerCommandSuffix = commandSuffix;
    const outerCommand = command; // the return value will be `result`; we will populate it with
    // those fields now; `body` is the flex-wrap portion of the
    // content

    const resultWrapper = div(undefined, 'usage-error-wrapper');
    const result = div(undefined, options.noHide ? '' : ['hideable', 'page-content'], 'p');
    const body = div();
    const left = div(); // usage and detailedExample

    const right = div(); // required and optional parameters
    // if we have a great many detailed examples, place them in a scroll region

    const scrollableDetailedExamples = detailedExample && Array.isArray(detailedExample) && detailedExample.length > 4;

    if (sections && sections.length > 1 || scrollableDetailedExamples && sections && sections.length > 0) {
      left.classList.add('fifty-fifty');
      right.classList.add('fifty-fifty');
    } else {
      left.style.marginRight = '2em';
    }

    if (message.message) {
      // then the repl wrapped around the usage model, adding an extra message string
      const messageString = message.message;
      const messageDom = div(undefined, '', 'div');
      const prefacePart = span('');
      const messagePart = span(messageString, 'red-text usage-error-message-string');

      if (message.messageDom) {
        if (typeof message.messageDom === 'string') {
          messagePart.appendChild(document.createTextNode(message.messageDom));
        } else {
          messagePart.appendChild(message.messageDom);
        }
      }

      if (!options.noHide) {
        result.classList.add('hidden');
      }

      messageDom.appendChild(prefacePart);
      messageDom.appendChild(messagePart);
      resultWrapper.appendChild(messageDom);

      if (!(0, _capabilities.isHeadless)() && !options.noHide) {
        const usagePart = div(undefined, 'small-top-pad hideable click-here-for-usage-container');
        const frontPart = span('Click ');
        const clickyPart = span('here', 'clickable clickable-blatant click-here-for-usage');
        const restPart = span(' for usage information.');
        usagePart.appendChild(frontPart);
        usagePart.appendChild(clickyPart);
        usagePart.appendChild(restPart);
        messageDom.appendChild(usagePart);

        clickyPart.onclick = () => {
          result.classList.remove('hidden');
          usagePart.classList.add('hidden');
        };
      }
    }

    resultWrapper.appendChild(result); //
    // breadcrumb
    //

    if (!options.noBreadcrumb) {
      // breadcrumb model chain
      const parentChain = (usage.parents || []).map(breadcrumb => ({
        breadcrumb
      }));
      const thisCommand = breadcrumb ? [{
        breadcrumb,
        noSlash: true,
        preserveCase
      }] : [];
      const breadcrumbs = [...parentChain, ...thisCommand]; // generate the breadcrumb Elements from the model

      const crumbs = yield promiseEach(breadcrumbs, makeBreadcrumb); // attach the breadcrumb to the view

      const container = div(undefined, 'bx--breadcrumb bx--breadcrumb--no-trailing-slash', 'h2');
      result.appendChild(container);
      crumbs.forEach(breadcrumb => container.appendChild(breadcrumb));
    } //
    // title
    //

    /* if (title) {
       const dom = div(title, 'capitalize', 'h1')
       dom.style.fontSize = '1.629em'
       dom.style.fontWeight = 300
       dom.style.color = 'var(--color-brand-01)'
       dom.style.margin = '0 0 .3rem'
       result.appendChild(dom)
       } */
    //
    // header message
    //


    if (header) {
      let headerDiv = div(header, 'normal-text sans-serif');

      if (!(0, _capabilities.isHeadless)()) {
        try {
          /* const renderer = new Marked.Renderer()
          const marked = (_: string): string => Marked(_, { renderer })
          renderer.link = (href: string, title: string, text: string) => {
            return `<a class='bx--link' target='_blank' title="${title}" href="${href}">${text}</a>`
          } */
          headerDiv = div('', 'normal-text sans-serif marked-content');
          headerDiv.innerText = header; // marked(header)
        } catch (err) {
          debug('error using marked', err);
        }
      }

      if (message.exitCode > 0 && message.exitCode !== 200) {
        // the underlying command emitted some sort of usage model in response to an error
        // e.g. `kubectl get pods --bogus
        headerDiv.classList.add('red-text');
      }

      result.appendChild(headerDiv);
    }

    body.style.display = 'flex';
    body.style.flexWrap = 'wrap';
    body.style.marginTop = '0.375em';
    body.appendChild(left);
    body.appendChild(right);
    result.appendChild(body); // keep track of the scroll regions we have created; if not
    // many, maybe we use outer scrolling inside of inner scrolling

    const scrollRegions = []; // any minimally formatted sections? e.g. `intro` and `section` fields

    const makeSection = (parent = right, noMargin = false) => ({
      title,
      content
    }) => {
      const wrapper = bodyPart(noMargin);
      const prePart = prefix(title);
      const contentPart = document.createElement('pre');
      contentPart.classList.add('pre-wrap');
      contentPart.innerText = content;
      wrapper.appendChild(prePart);
      wrapper.appendChild(contentPart);
      parent.appendChild(wrapper);
    }; // top-most intro section?


    if (intro) {
      makeSection(left)(intro);
    } // example command


    if (example) {
      const examplePart = bodyPart();
      const prePart = prefix('Usage');
      const html = example.trim().replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/(\[[^[\]]+\])/g, '<span class="even-lighter-text">$1</span>'); // lighter text for [optional args]

      const textPart = div();
      textPart.innerHTML = html;
      left.appendChild(examplePart);
      examplePart.appendChild(prePart);
      examplePart.appendChild(textPart);
      textPart.style.color = 'var(--color-base0C)';
    } // detailed example command


    if (detailedExample) {
      const examples = !Array.isArray(detailedExample) ? [detailedExample] : detailedExample;
      const examplePart = bodyPart();
      const prePart = prefix(examples.length === 1 ? 'Example' : 'Examples');
      left.appendChild(examplePart);
      examplePart.appendChild(prePart); // only if we there are other sections to show, render the
      // detailed examples in a scroll region

      const examplesInScrollRegion = examples.length > 4 && sections && sections.length > 0;
      const rowsPart = examplesInScrollRegion ? div(undefined, ['scrollable', 'scrollable-auto']) : examplePart;

      if (examplesInScrollRegion) {
        scrollRegions.push(rowsPart);
        const nRowsInViewport = 6;
        examplePart.appendChild(rowsPart);
        rowsPart.style.maxHeight = `calc(${nRowsInViewport} * 3em + 1px)`;
      }

      examples.forEach(({
        command,
        docs
      }) => {
        const exampleContainer = div(undefined, 'present-as-quotation');
        rowsPart.appendChild(exampleContainer);
        const textPart = div(command);
        const docPart = sans(div(docs, 'lighter-text'));
        exampleContainer.appendChild(textPart);
        exampleContainer.appendChild(docPart);
        textPart.classList.add('example-command-text');
      });
    }
    /**
     * Render a table of options
     *
     */


    const makeTable = (title, rows, parent = right, nRowsInViewport = usage && usage.nRowsInViewport || 5) => {
      const wrapper = bodyPart();
      const prePart = prefix(title);
      wrapper.appendChild(prePart);
      parent.appendChild(wrapper);

      if (typeof rows === 'string') {
        // then we have a degenerate case where the "rows" is a
        // plain string
        debug('plain string body', rows);
        const content = document.createElement('div');
        content.innerText = rows;
        wrapper.appendChild(content);
        return;
      }

      const table = document.createElement('table');
      table.className = 'log-lines'; // make the table scrollable, showing only a max number of
      // rows at a time: e.g. 5 rows, plus a bit (1px) for the
      // bottom border; the 3em part must .log-line's height in
      // ui.css; nRowsInViewport = true means disable inner scrolling

      if (rows.length > nRowsInViewport && nRowsInViewport !== true) {
        const tableScrollable = div(undefined, ['scrollable', 'scrollable-auto']);
        const nRows = sections && (sections.length === 2 || sections.length === 1 && scrollableDetailedExamples) ? 8 : nRowsInViewport;
        tableScrollable.style.maxHeight = `calc(${nRows} * 3em + 1px)`;
        tableScrollable.appendChild(table);
        wrapper.appendChild(tableScrollable);
        scrollRegions.push(tableScrollable);
      } else {
        wrapper.appendChild(table);
      } // render the rows


      const renderRow = rowData => {
        if (isGenerator(rowData)) {
          // then rowData is a generator for aliases
          renderRow(rowData.fn(rowData.command));
          return;
        }

        const isDir = rowData.available !== undefined || false; // fields of the row model
        // debug('row', rowData)

        const {
          commandPrefix = outerCommandPrefix,
          commandSuffix = outerCommandSuffix,
          command = outerCommand,
          name = command,
          label = name,
          noclick = false,
          synonyms,
          alias,
          numeric,
          aliases = (synonyms || [alias]).filter(x => x),
          hidden = false,
          advanced = false,
          example = numeric && 'N',
          title,
          header,
          docs = header || title,
          allowed,
          defaultValue
        } = rowData; // row is either hidden or only shown for advanced users

        if (hidden) return;
        if (advanced) return; // for now

        const row = table.insertRow(-1);
        row.className = 'log-line entity';
        const cmdCell = row.insertCell(-1);
        const docsCell = row.insertCell(-1);
        const cmdPart = span(label && label.replace(/=/g, '=\u00ad'), 'pre-wrap');
        const dirPart = isDir && label && span('/');
        const examplePart = example && span(example, label || dirPart ? 'left-pad lighter-text smaller-text' : ''); // for -p key value, "key value"

        const aliasesPart = aliases && aliases.length > 0 && span(undefined, 'lighter-text smaller-text small-left-pad');
        const docsPart = span(docs);
        const allowedPart = allowed && smaller(span(undefined)); // for repl.exec,

        const commandForExec = (alias, cmd = '') => {
          return `${commandPrefix && !commandPrefixNotNeeded ? commandPrefix + ' ' : ''}${alias} ${cmd} ${commandSuffix}`;
        };

        cmdCell.className = 'log-field';
        docsCell.className = 'log-field';
        cmdPart.style.fontWeight = '500';
        wrap(sans(docsPart)); // command aliases

        if (aliases) {
          aliases.filter(x => x).forEach(alias => {
            const cmdCell = span();
            const cmdPart = span(alias
            /* noclick ? '' : 'clickable clickable-blatant' */
            ); // don't make aliases clickable

            const dirPart = isDir && span('/');

            if (!noclick) {
              cmdPart.onclick = pexec(commandForExec(alias));
            }

            aliasesPart.appendChild(cmdCell);
            cmdCell.appendChild(cmdPart);
            if (dirPart) cmdCell.appendChild(smaller(dirPart));
          });
        } // allowed and default values


        if (allowed) {
          allowedPart.style.color = 'var(--color-text-02)';
          allowedPart.appendChild(span('options: '));
          allowed.forEach((value, idx) => {
            const option = span(`${idx > 0 ? ', ' : ''}${value}${value !== defaultValue ? '' : '*'}`);
            allowedPart.appendChild(option);
          });
        }

        cmdCell.appendChild(cmdPart);
        if (dirPart) cmdCell.appendChild(smaller(dirPart));
        if (aliasesPart) cmdCell.appendChild(smaller(aliasesPart));
        if (examplePart) cmdCell.appendChild(examplePart);
        docsCell.appendChild(docsPart);
        if (allowedPart) docsCell.appendChild(allowedPart);

        if (command) {
          if (!isDir) cmdPart.classList.add('semi-bold');

          if (!noclick) {
            cmdPart.classList.add('clickable');
            cmdPart.classList.add('clickable-blatant');
            cmdPart.setAttribute('tabindex', '0');

            cmdPart.onclick = () => __awaiter(void 0, void 0, void 0, function* () {
              return pexec(commandForExec(command, name !== command ? name : undefined))();
            });
          }
        }
      };
      /* renderRow */


      rows.forEach(renderRow);
      return wrapper;
    };

    if (sections) {
      // render more general sections, rather than the specific
      // oneof, optional, etc.
      const defaultNRowsInViewport = (idx, nRows) => {
        if (idx === sections.length - 1 && sections.length % 2 === 0) {
          return sections.length === 2 ? nRows : 5;
        } // otherwise, accept global default

      };

      const stringSections = sections.filter(_ => typeof _.rows === 'string');
      const tableSections = sections.filter(_ => Array.isArray(_.rows));
      debug('string', stringSections);
      debug('table', tableSections);
      tableSections.sort(({
        rows: a
      }, {
        rows: b
      }) => a.length - b.length);

      const nRowsOf = (section, idx) => {
        debug('nRowsOf', section, section.rows.length, section.nRowsInViewport, defaultNRowsInViewport(idx, section.rows.length));
        return Math.min(section.rows.length, section.nRowsInViewport || defaultNRowsInViewport(idx, section.rows.length) || section.rows.length);
      };

      const totalRows = tableSections.reduce((total, section, idx) => {
        return total + nRowsOf(section, idx);
      }, 0);
      stringSections.forEach(section => {
        const {
          title,
          rows
        } = section;
        makeTable(title, rows, left);
      });
      let runningSum = 0;
      tableSections.forEach((section, idx) => {
        const {
          title,
          rows,
          nRowsInViewport
        } = section;
        runningSum += nRowsOf(section, idx);
        debug('running', runningSum, totalRows);
        makeTable(title, rows, runningSum < totalRows / 2 ? left : right, nRowsInViewport || defaultNRowsInViewport(idx, rows.length));
      });
    }

    if (available) {
      const table = makeTable('Available Commands', available);
      table.classList.add('user-error-available-commands');

      if (!title && !header) {
        table.style.marginTop = '0';
      }
    }

    if (required) {
      makeTable('Required Parameters', required);
    }

    if (oneof) {
      makeTable('Required Parameters (choose one of the following)', oneof);
    }

    if (optional) {
      makeTable('Optional Parameters', optional);
    }

    if (sampleInputs) {
      // attach this to the left side, along with usage
      makeTable('Sample Inputs', sampleInputs, left);
    }

    if (related) {
      const relatedPart = bodyPart();
      const prePart = prefix('Related Commands');
      const listPart = div();
      relatedPart.appendChild(prePart);
      relatedPart.appendChild(listPart);
      result.appendChild(relatedPart); // note that we append to result not body; body is for the flex-wrap bits

      related.forEach((command, idx) => {
        const commandPart = span(undefined, '');
        const commaPart = span(idx === 0 ? '' : ', ', '');
        const clickablePart = span(command, 'clickable');
        commandPart.appendChild(commaPart);
        commandPart.appendChild(clickablePart);
        clickablePart.onclick = pexec(command);
        listPart.appendChild(commandPart);
      });
    } // only one scroll region? then we might as well use outer scrolling


    if (scrollRegions.length === 1) {
      scrollRegions[0].classList.remove('scrollable');
      scrollRegions[0].classList.remove('scrollable-auto');
      scrollRegions[0].style.maxHeight = '';
    }

    return resultWrapper;
  }
});

function isMessageWithCode(msg) {
  const message = msg;
  return !!(message.message && (message.code || message.statusCode || message.exitCode));
}

class UsageError extends Error {
  constructor(message, extra) {
    super();

    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }

    this.raw = message;
    this.code = isMessageWithCode(message) ? message.statusCode || message.code || message.exitCode : 500;

    if (typeof message === 'string') {
      this.message = message;
    } else {
      this.formattedMessage = format(message, extra);
    }
  }

  getUsageModel() {
    return this.raw.usage;
  }

  static getFormattedMessage(err) {
    if (err.formattedMessage && !err.formattedMessage.then && Object.keys(err.formattedMessage).length === 0) {
      err.formattedMessage = format(err.raw);
    }

    return err.formattedMessage ? err.formattedMessage : Promise.resolve(span(err.message));
  }

  static isUsageError(error) {
    const err = error;
    return !!(err.formattedMessage && err.code);
  }

}

exports.UsageError = UsageError;

function isUsageError(error) {
  return UsageError.isUsageError(error);
}

var _default = UsageError;
exports.default = _default;