text: /*
   * Copyright 2019 Adobe. All rights reserved.
   * This file is licensed to you 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 REPRESENTATIONS
   * OF ANY KIND, either express or implied. See the License for the specific language
   * governing permissions and limitations under the License.
   */
  
  /* eslint-disable no-param-reassign */
  
  const i18n = require('i18n');
  const Promise = require('bluebird');
  const path = require('path');
  const _ = require('lodash');
  const ejs = require('ejs');
  const { info } = require('@adobe/helix-log');
  
  const pejs = Promise.promisifyAll(ejs);
  const GithubSlugger = require('github-slugger');
  const { headers } = require('./header');
  const prettyMarkdown = require('./prettyMarkdown');
  const writeFile = require('./writeFiles');
  
  function createGithubSlugs(names) {
    const slugger = new GithubSlugger();
    slugger.reset();
    names = Array.from(names).sort();
    return names.reduce((result, item) => {
      result[item] = slugger.slug(item);
      return result;
    }, {});
  }
  
  function render([template, context]) {
    return pejs.renderFileAsync(template, context, { debug: false });
  }
  
  function build(total, fragment) {
    return total + fragment.replace(/\n\n/g, '\n');
  }
  
  function assoc(obj, key, value) {
    if (obj === null) {
      return assoc({}, key, value);
    }
    obj[key] = value;
    return obj;
  }
  
  function flatten(dependencies) {
    let deps = [];
    if (dependencies) {
      const key = _.keys(dependencies)[0];
      deps = _.toPairs(dependencies[key]).map(([first, second]) => {
        second.$id = first;
        return second;
      });
    }
    return deps;
  }
  
  function stringifyExamples(examples) {
    if (examples) {
      if (typeof examples === 'string') {
        examples = [examples];
      }
      return examples.map(example => JSON.stringify(example, null, 2));
    } else {
      return false;
    }
  }
  
  /**
   * Finds a simple, one-line description of the property's type
   * @param {object} prop - a JSON Schema property definition
   */
  function simpletype(prop) {
    const { type } = prop;
    if (prop.$ref !== undefined) {
      if (prop.$linkVal !== undefined) {
        prop.simpletype = prop.$linkVal;
      } else {
        info(`unresolved reference: ${prop.$ref}`);
        prop.simpletype = 'reference';
      }
    } else if (prop.enum !== undefined) {
      prop.simpletype = '`enum`';
      if (prop['meta:enum'] === undefined) {
        prop['meta:enum'] = {};
      }
      for (let i = 0; i < prop.enum.length; i += 1) {
        if (prop['meta:enum'][prop.enum[i]] === undefined) {
          //  setting an empty description for each unknown enum
          prop['meta:enum'][prop.enum[i]] = '';
        }
      }
    } else if (prop.const !== undefined) {
      prop.simpletype = '`const`';
    } else if (type === 'string') {
      prop.simpletype = '`string`';
    } else if (type === 'number') {
      prop.simpletype = '`number`';
    } else if (type === 'boolean') {
      prop.simpletype = '`boolean`';
    } else if (type === 'integer') {
      prop.simpletype = '`integer`';
    } else if (type === 'object') {
      prop.simpletype = '`object`';
    } else if (type === 'null') {
      prop.simpletype = '`null`';
    } else if (type === 'array') {
      if (prop.items !== undefined) {
        const innertype = simpletype(prop.items);
        if (innertype.simpletype === 'complex') {
          prop.simpletype = '`array`';
        } else {
          prop.simpletype = innertype.simpletype.replace(/(`)$/, '[]$1');
        }
      } else {
        prop.simpletype = '`array`';
      }
    } else if (Array.isArray(type)) {
      const nullfilter = str => str !== 'null';
      const filtered = type.filter(nullfilter);
      if (type.length - 1 === filtered.length) {
        prop.nullable = true;
      }
      if (filtered.length === 1) {
        ([prop.type] = filtered);
        prop.simpletype = `\`${filtered[0]}\``;
      } else {
        prop.type = filtered;
        prop.simpletype = 'multiple';
      }
    } else {
      prop.simpletype = 'complex';
    }
    return prop;
  }
  /**
   * Combines the `required` array data structure with the `properties` map data
   * structure, so that each property in `properties` that is required, i.e. listed
   * as a value in the `required` array will have an additional property `isrequired`
   * @param {*} properties
   * @param {*} required
   */
  function requiredProperties(properties, required) {
    if (required) {
      for (const prop of required) {
        if (properties[prop]) {
          properties[prop].isrequired = true;
        }
      }
    }
    return _.mapValues(properties, simpletype);
  }
  
  function ejsRender(template, ctx) {
    const p = pejs.renderFileAsync(path.join(__dirname, `../templates/md/${template}.ejs`), ctx, { debug: false });
    return p.value();
    //  return JSON.stringify(obj, null, 2);
  }
  
  const generateMarkdown = (filename, schema, schemaPath, outDir,
    dependencyMap, docs, consoleArgs) => {
    outDir = outDir || path.resolve(path.join('.', 'out'));
    let customElements = [];
    if (consoleArgs !== undefined && consoleArgs.p !== undefined) {
      customElements = consoleArgs.p.split(',');
    }
    // this structure allows us to have separate templates for each element. Instead of having
    // one huge template, each block can be built individually
    outDir = outDir || path.resolve(path.join('.', 'out'));
    let multi = [];
    multi.push(['frontmatter.ejs', { meta: schema.metaElements }]);
    if (!consoleArgs || consoleArgs.header) {
      multi.push(['header.ejs', {
        i18n,
        schema,
        dependencies: flatten(dependencyMap),
        table: headers(schema, schemaPath, filename, docs).render(),
      }]);
    }
    multi.push(['examples.ejs', { examples: stringifyExamples(schema.examples), title: schema.title, i18n }]);
    const required = []; //  to store required of whole schema, even those in definitions
  
    // Processing schema.definitions before schema.properties to get any
    // required properties present in definitions
    if (_.keys(schema.definitions).length > 0) {
      const abstract = {};
      for (let i = 0; i < _.keys(schema.definitions).length; i += 1) {
        if (schema.definitions[_.keys(schema.definitions)[i]].properties !== undefined) {
          const definition = schema.definitions[_.keys(schema.definitions)[i]].properties;
          const tempRequired = schema.definitions[_.keys(schema.definitions)[i]].required;
          const hasRequiredProperties = (tempRequired !== undefined);
          for (let j = 0; j < _.keys(definition).length; j += 1) {
            const name = _.keys(definition)[j];
            const property = definition[_.keys(definition)[j]];
            if (hasRequiredProperties && tempRequired.includes(name)) {
              required.push(name);
            }
            if (_.keys(schema.properties).indexOf(name) === -1) {
              property.definitiongroup = _.keys(schema.definitions)[i];
              abstract[name] = property;
            }
          }
        }
      }
      const propertiesSlugs = createGithubSlugs(_.keys(abstract));
      if (_.keys(abstract).length > 0) {
        multi.push(['definitions.ejs', {
          props: requiredProperties(abstract, required),
          title: schema.title,
          id: schema.$id,
          propertiesSlugs,
          i18n,
        }]);
        for (let i = 0; i < _.keys(abstract).length; i += 1) {
          const name = _.keys(abstract).sort()[i];
          multi.push(['property.ejs', {
            name,
            required: required.includes(name),
            ejs: ejsRender,
            examples: stringifyExamples(abstract[name].examples),
            schema: simpletype(abstract[name]),
            nameSlug: propertiesSlugs[name],
            i18n,
            customElements,
          }]);
        }
      }
    }
    const propertiesSlugs = createGithubSlugs(_.keys(schema.properties));
    if (_.keys(schema.properties).length > 0) {
      if (schema.required === undefined) {
        schema.required = [];
      }
      schema.required = _.union(schema.required, required);
      // table of contents
      multi.push(['properties.ejs', {
        props: requiredProperties(schema.properties, schema.required),
        pprops: _.mapValues(schema.patternProperties, simpletype),
        title: schema.title,
        additional: schema.additionalProperties,
        propertiesSlugs,
        i18n,
      }]);
      // regular properties
      for (let i = 0; i < _.keys(schema.properties).length; i += 1) {
        const name = _.keys(schema.properties).sort()[i];
        multi.push(['property.ejs', {
          name,
          required: schema.required ? schema.required.includes(name) : false,
          examples: stringifyExamples(schema.properties[name].examples),
          ejs: ejsRender,
          schema: simpletype(schema.properties[name]),
          nameSlug: propertiesSlugs[name],
          i18n,
          customElements,
        }]);
      }
    }
  
    if (_.keys(schema.patternProperties).length > 0) {
      // patterns properties
      for (let i = 0; i < _.keys(schema.patternProperties).length; i += 1) {
        const name = _.keys(schema.patternProperties)[i];
        multi.push(['pattern-property.ejs', {
          name,
          examples: stringifyExamples(schema.patternProperties[name].examples),
          ejs: ejsRender,
          schema: simpletype(schema.patternProperties[name]),
          i18n,
          customElements,
        }]);
      }
    }
  
    // Handles join-type properties
    // If the schema contains a 'oneOf', 'allOf' or 'anyOf'.
    const joinTypeKey = Object.keys(schema).find(key => ['oneOf', 'allOf', 'anyOf'].indexOf(key) > -1);
    if (!_.isUndefined(joinTypeKey)) {
      const joinType = schema[joinTypeKey];
      if (joinType.length > 0) {
        multi.push(['join-type.ejs', {
          ejs: ejsRender,
          schemas: simpletype(joinType),
          schema: simpletype(schema),
          i18n,
          customElements,
        }]);
      }
    }
    // find definitions that contain properties that are not part of the main schema
    multi = multi.map(([template, context]) => [
      path.join(__dirname, `../templates/md/${template}`),
      assoc(assoc(context, '_', _), 'simpletype', simpletype),
    ]);
  
    return Promise.reduce(Promise.map(multi, render), build, '').then((str) => {
      const mdfile = `${path.basename(filename).slice(0, -5)}.md`;
      return writeFile(
        path.join(outDir, path.dirname(filename.substr(schemaPath.length))),
        mdfile,
        prettyMarkdown(str),
      );
    });
  };
  
  module.exports = generateMarkdown;
  