/**
 * Copyright (c) HashiCorp, Inc.
 * SPDX-License-Identifier: BUSL-1.1
 */

/* eslint-env node */

/**
 * codemod to transform button html element to Hds::Button component
 * transformation is skipped if is-ghost or is-transparent is found in class list
 * if loading or is-loading is found to be a conditionally applied class the loading icon will be conditionally applied instead
 * if the text arg cannot be built from the child nodes (chained if block or multiple nodes that cannot be easily combined) the transformation will be skipped
 * classes relevant to the legacy button will be removed (see classesToRemove array)
 * html onclick event handler will be replaced with the {{on "click"}} modifier
 *
 * example execution from ui directory:
 ** -> npx ember-template-recast ./app/templates -t ./scripts/codemods/hds/button.js
 * for best results run prettier after:
 ** -> npx ember-template-recast ./app/templates -t ./scripts/codemods/hds/button.js && npx prettier --config .prettierrc.js --write ./app/templates
 */

class Transforms {
  // button classes that will be removed from attribute
  classesToRemove = [
    'button',
    'is-compact',
    'is-danger',
    'is-danger-outlined',
    'is-flat',
    'is-icon',
    'is-loading',
    'is-link',
    'is-primary',
    'tool-tip-trigger',
    'is-secondary',
  ];
  classesToTransform = [{ current: 'toolbar-link', updated: 'toolbar-button' }];

  constructor(node, builders) {
    this.node = node;
    this.attrs = [];
    this.modifiers = [...node.modifiers];
    this.builders = builders;
    this.hasIcon = false;
    this.hasText = false;
  }

  shouldTransform() {
    // buttons that have the is-ghost and/or is-transparent class will not be transformed
    // these usages have unclear mappings to tertiary buttons and in some cases will be replaced with Hds::Interactive
    const classAttr = this.node.attributes.find((attr) => attr.name === 'class');
    if (classAttr) {
      const shouldTransform = (chars) => {
        return chars.includes('is-ghost') || chars.includes('is-transparent') ? false : true;
      };
      if (classAttr.value.type === 'ConcatStatement') {
        for (const part of classAttr.value.parts) {
          if (part.type === 'TextNode' && !shouldTransform(part.chars)) {
            return false;
          }
        }
      } else {
        return shouldTransform(classAttr.value.chars);
      }
    }
    return true;
  }

  addAttr(name, value) {
    this.attrs.push(this.builders.attr(name, value));
  }

  filterClassTextNode(value) {
    // map color related classes to @color args
    let color = 'secondary'; // currently the default for .button class
    for (const colorClass of ['is-primary', 'is-danger', 'is-danger-outlined']) {
      if (value.chars.includes(colorClass)) {
        color = colorClass === 'is-primary' ? null : 'critical';
        break;
      }
    }
    if (color) {
      this.addAttr('@color', this.builders.text(color));
    }
    // remove button related classes no longer needed
    // map unused classes to new ones
    const classArray = value.chars.split(' ');
    const chars = classArray
      .filter((className) => !this.classesToRemove.includes(className))
      .map((className) => {
        const transform = this.classesToTransform.find((classHash) => classHash.current === className);
        return transform?.updated || className;
      })
      .join(' ');
    return chars ? { ...value, chars } : null;
  }

  convertIsLoadingMustache(part, filteredParts) {
    let isLoading = false;
    const filteredParams = part.params.map((param) => {
      if (param.type === 'StringLiteral' && param.value.includes('loading')) {
        // rebuild param since icon name is loading and class name could be is-loading
        isLoading = true;
        return this.builders.string('loading');
      }
      return param;
    });
    if (isLoading) {
      this.addAttr('@icon', this.builders.mustache('if', filteredParams));
    } else {
      filteredParts.push(part);
    }
  }

  filterClassConcatStatement(attr) {
    const filteredParts = [];
    attr.value.parts.forEach((part) => {
      if (part.type === 'TextNode') {
        const value = this.filterClassTextNode(part);
        if (value) {
          filteredParts.push(value);
        }
      } else if (part.type === 'MustacheStatement') {
        this.convertIsLoadingMustache(part, filteredParts);
      } else {
        filteredParts.push(part);
      }
    });
    if (filteredParts.length) {
      return filteredParts.length === 1 ? filteredParts[0] : { ...attr.value, parts: filteredParts };
    }
  }

  filterClasses(attr) {
    if (attr.name === 'class') {
      let attrValue = attr.value;
      const { type } = attrValue;
      if (type === 'ConcatStatement') {
        attrValue = this.filterClassConcatStatement(attr);
      } else if (type === 'TextNode') {
        attrValue = this.filterClassTextNode(attr.value);
      }
      if (attrValue) {
        this.addAttr('class', attrValue);
      }
    }
  }

  convertOnClick(attr) {
    const params = [this.builders.string('click')];
    if (!attr.value.params.length) {
      params.push(attr.value.path);
    } else {
      params.push(this.builders.sexpr(attr.value.path, attr.value.params));
    }
    const onClickModifier = this.builders.elementModifier('on', params);
    this.modifiers.push(onClickModifier);
  }

  filterAttributes() {
    this.node.attributes.forEach((attr) => {
      if (attr.name === 'class') {
        return this.filterClasses(attr);
      } else if (attr.name === 'onclick') {
        return this.convertOnClick(attr);
      } else if (attr.name === 'type' && attr.value.chars === 'button') {
        // remove type="button" attribute since it is default
        return;
      }
      this.attrs.push(attr);
    });
  }

  filterModifiers() {
    const params = [this.builders.string('click')];
    this.node.modifiers.forEach((modifier) => {
      if (modifier.path === 'action') {
        // Replaces {{action "blah"}} with {{on "click" (action "blah")}}
        params.push(this.builders.sexpr(modifier.path, modifier.params));
        const onClickModifier = this.builders.elementModifier('on', params);
        this.modifiers.push(onClickModifier);
      }
    });
  }

  textToString(node) {
    // filter out escape charaters like \n and whitespace from TextNode and rebuild as StringLiteral
    const text = decodeURI(node.chars).trim();
    if (text) {
      return this.builders.string(text);
    }
  }

  filterTextNode(node, parts) {
    if (node.type === 'TextNode') {
      const text = this.textToString(node);
      if (text) {
        parts.push(text);
      }
    }
  }

  convertBlockStatementNode(node, parts) {
    // convert if/else block statement to inline if mustache
    if (node.type === 'BlockStatement' && node.path.original === 'if' && !node.inverse.chained) {
      // only deal with text nodes -- more complex expressions should be converted to getter on component
      const program = node.program.body;
      const ifValueNode = program.length === 1 && program[0].type === 'TextNode' ? program[0] : null;
      const inverse = node.inverse.body;
      const elseValueNode = inverse.length === 1 && inverse[0].type === 'TextNode' ? inverse[0] : null;

      if (ifValueNode && elseValueNode) {
        const params = [...node.params, this.textToString(ifValueNode), this.textToString(elseValueNode)];
        parts.push(this.builders.mustache(node.path, params));
      }
    }
  }

  convertIconNode(node) {
    if (node.tag === 'Icon') {
      const nameAttr = node.attributes.find((attr) => attr.name === '@name');
      this.addAttr('@icon', this.builders.string(nameAttr.value.chars));
      // Hds::Button has @iconPosition arg when used with text
      // it seems most usages with button are leading which is default and recommended
      this.hasIcon = true;
    }
  }

  pushAcceptedNodes(node, parts) {
    // some nodes may not need conversion and can be added to the @text assembly as is
    const acceptedNodes = ['MustacheStatement'];
    if (acceptedNodes.includes(node.type)) {
      parts.push(node);
    }
  }

  childNodesToArgs() {
    // convert child nodes to a format supported by an attr value for @text arg
    const parts = [];
    this.node.children.forEach((node) => {
      // following methods are used to build the @text arg
      this.filterTextNode(node, parts);
      this.convertBlockStatementNode(node, parts);
      this.pushAcceptedNodes(node, parts);
      // we also need to set the icon related args
      this.convertIconNode(node);
    });

    // filter out ignored text nodes (\n) and compare with out compiled parts
    // if the lengths do not match then we were unable to transform a part and we must abort text build
    const relevantParts = this.node.children.filter((node) => {
      if (node.type === 'TextNode' && !this.textToString(node)) {
        return false;
      }
      return true;
    });
    if (parts.length && relevantParts.length === parts.length) {
      const value = parts.length === 1 ? parts[0] : this.builders.concat(parts);
      this.addAttr('@text', value);
      this.hasText = true;
    } else if (this.hasIcon) {
      // if there was an icon node but no text we need to add the @isIconOnly arg
      this.addAttr('@isIconOnly', this.builders.mustache(this.builders.boolean(true)));
      this.addAttr('@text', 'REPLACE_ME');
    }
  }

  buildElement() {
    if (this.hasText || this.hasIcon) {
      return this.builders.element(
        { name: 'Hds::Button', selfClosing: true },
        { attrs: this.attrs, modifiers: this.modifiers }
      );
    }
  }
}

module.exports = (env) => {
  const { builders } = env.syntax;

  return {
    ElementNode(node) {
      if (node.tag === 'button') {
        try {
          const transforms = new Transforms(node, builders);
          if (transforms.shouldTransform()) {
            transforms.childNodesToArgs();
            transforms.filterAttributes();
            transforms.filterModifiers();
            return transforms.buildElement();
          }
        } catch (error) {
          console.log(`\nError caught transforming button in ${env.filePath}\n`, error); // eslint-disable-line
        }
      }
    },
  };
};
