/**
 * Create component export default class with general(repeating) methods.
 * The Component export default class is used as parent class.
 */
export default class Component {
  /**
    * Constructor.
    * @param {string| Component object } parent - Expects parent
    * @param {string} selector - Expects xpath
    */
  constructor(parent = '', selector) {
    if (parent.currentPath) {
      parent = parent.currentPath;
    }
    /** @type {string} */
    this.parent = parent;
    /** @type {string} */
    this.selector = selector;
    /** @type {string} */
    if (this.selector) {
      this.currentPath = parent + '//' + selector;
    } else {
      this.currentPath = parent;
    }
  }
  /**
   * This method returns WebdriverIO element from component.
   * @async
   */
  async element() {
    return await $(this.currentPath);
  }
  /**
   * This method returns list of elements from component.
   * @async
   */
  async elementList() {
    return await $$(this.currentPath);
  }
  /**
   * This method sets the current component element to the default xpath.
   * The parent remains unchanged.
   * @returns {this} -  Returns current component
   */
  byDefault() {
    this.currentPath = this.parent + '//' + this.selector;
    return this;
  }
  /**
   * This method sets the current component element to the one with the given index of the parent and selector element
   * The parent remains unchanged
   * @param {number} index - Index number of element
   * @returns {this} - Returns current component set by index
   */
  byIndex(index) {
    this.currentPath = '(' + this.parent + '//' + this.selector + ')[' + index + ']';
    return this;
  }
  /**
   * This method sets the current component element to the one with the given index of the current element
   * The parent remains unchanged
   * @param {number} index - Index number of element
   * @returns {this} - Returns current component set to index
   */
  andByIndex(index) {
    this.currentPath = '(' + this.currentPath + ')[' + index + ']';
    return this;
  }
  /**
   * This method sets the current component element to parent and selector with given attribute equals the given value [@attr="value"] and returns the updated component.
   * The parent remains unchanged
   * @param {string} attr -Element attribute eg : id, name, class
   * @param {string} value - Element attribute value eg : readonly
   * @returns {this} - Returns current component sets to attribute equal value
   */
  byAttributeEquals(attr, value) {
    this.currentPath = this.parent + '//' + this.selector + '[@' + attr + '="' + value + '"]';
    return this;
  }
  /**
   * This method sets the current component element to parent and selector where the given attribute value ends with the given valueEnding and returns the updated component.
   * The parent remains unchanged
   * @param {string} attr - Element attribute eg : id, name, class
   * @param {string} valueEnding - Element attribute value ending
   * @returns {this} - Returns current component set to attribute value ends with
   */
  byAttributeValueEndsWith(attr, valueEnding) {
    this.currentPath = this.parent + '//' + this.selector + '[substring(@' + attr + ',string-length(@' + attr + ') -string-length("' + valueEnding + '") +1) = "' + valueEnding + '"]';
    return this;
  }
  /**
   * This method sets the current component element to parent and selector by given id value and returns the updated component.
   * The parent remains unchanged
   * @param {string} id - Value of attribute id
   * @returns {this} - Returns current component set to id value
   */
  byId(id) {
    this.currentPath = this.parent + '//' + this.selector + '[@id="' + id + '"]';
    return this;
  }
  /**
   * This method sets the current component element to parent and selector where the given id value starts with and returns the updated component.
   * The parent remains unchanged
   * @param {string} id - Value of attribute id
   * @returns {this} - Returns current component set to id that value starts with given value
   */
  byIdValueStartsWith(id) {
    this.currentPath = this.parent + '//' + this.selector + '/div[starts-with(@id,"' + id + '")]';
    return this;
  }
  /**
   * This method sets the current component element to parent and selector where the given attribute contains the given value [contains(@attr, 'value')]
   * and returns the updated component.
   * The parent remains unchanged
   * @param {string} attr - Element attribute eg : id, name, class
   * @param {string} value - Element attribute value eg : readonly
   * @returns {this} - Returns current component set to attribute that contains given value
   */
  byAttributeContains(attr, value) {
    this.currentPath = this.parent + '//' + this.selector + '[contains(@' + attr + ',"' + value + '")]';
    return this;
  }

  /**
   * This method sets the current component element to parent and selector where the given attribute do not contains the given value [not contains(@attr, 'value')]
   * and returns the updated component.
   * The parent remains unchanged
   * @param {string} attr -element attribute eg : id, name, class
   * @param {string} valueNotToBeContained - value not contained in element attribute
   * @returns {this} - Returns current component set to attribute that not contains given value
   */
  byAttributeNotContains(attr, valueNotToBeContained) {
    this.currentPath = this.parent + '//' + this.selector + '[not contains(@' + attr + ',"' + valueNotToBeContained + '")]';
    return this;
  }

  /**
   * This method sets the current component element to parent and selector where the element or a descendant equals the given text and returns the updated component.
   * By default html text is normalized for comparison, add false param if not desired.
   * The parent remains unchanged
   * @param {string} text - Given text
   * @param {boolean} normalize - If true removes empty text
   * @returns {this} - Returns current component set by text equal given value
   */
  byTextEquals(text, normalize = true) {
    this.currentPath = this.parent + '//' + this.selector
    if (normalize) {
      this.currentPath += '/descendant-or-self::*[text()[normalize-space()="' + text + '"]]/ancestor-or-self::' + this.selector + '[1]';
    } else {
      this.currentPath += '/descendant-or-self::*[text()="' + text + '"]/ancestor-or-self::' + this.selector + '[1]';
    }
    return this;
  }
  /**
   * This method sets the current component element to parent and selector where the element or a descendant contains the given text and returns the updated component.
   * By default html text is normalized for comparison, add false param if not desired.
   * The parent remains unchanged
   * @param {string} text - Given text
   * @param {boolean} normalize - If true removes empty text
   * @returns {this} - Returns current component set by text contains given value
   */
  byTextContains(text, normalize = true) {
    this.currentPath = this.parent + '//' + this.selector
    if (normalize) {
      this.currentPath += '/descendant-or-self::*[contains(normalize-space(), "' + text + '")]/ancestor-or-self::' + this.selector + '[1]';
    } else {
      this.currentPath += '/descendant-or-self::*[contains(text(),"' + text + '")]/ancestor-or-self::' + this.selector + '[1]';
    }
    return this;
  }
  /**
   * This method sets the current component element by text equal given value and by given index number and returns the updated component.
   * @param {string} text - Exact text of the component
   * @param {number} index - Index of the component in the array of ones containing text
   * @returns {this} - Returns current component set by text equal and by index
   */
  byTextEqualsAndIndex(text, index, normalize = true) {
    this.byTextEquals(text, normalize).andByIndex(index);
    return this;
  }
  /**
   * This method sets the current component element by text contains given value and by given index number and returns the updated component.
   * @param {string} text -Text of the component
   * @param {number} index - Index of the component in the array of ones containing text
   * @returns {this} - Returns current component set by text contains and by index
   */
  byTextContainsAndIndex(text, index, normalize = true) {
    this.byTextContains(text, normalize).andByIndex(index);
    return this;
  }

  /**
   * This method returns the number of components from the parent.
   * It uses the parent and selector used in the constructor.
   * @async
   * @returns {number} - Returns number of components
   */
  async defaultComponentsCount() {
    return await (await $$(this.parent + '//' + this.selector)).length;
  }

  /**
   * This method sets the current component element by child selector and returns the updated component.
   * @param {string} childSelector - expects xpath
   * @returns {this} - Returns current component set by child selector
   */
  byChildSelector(childSelector) {
    this.currentPath = this.parent + '//' + this.selector + '/descendant::' + childSelector + '/ancestor::' + this.selector + '[1]';
    return this;
  }

  /**
   * This method sets the current component element by attribute contains given value and by text contains given value and returns the updated component.
   * @param {string} attr -element attribute eg : id, name, class
   * @param {string} value - element attribute value eg : readonly
   * @param {string} text - given text
   * @param {boolean} normalize - if true removes empty text
   * @returns {this} - Returns current component set by attribute contains and text contains
   */
  byAttributeContainsAndTextContains(attr, value, text, normalize = true) {
    this.currentPath = this.parent + '//' + this.selector + '[contains(@' + attr + ',"' + value + '")]';
    if (normalize) {
      this.currentPath += '/descendant-or-self::*[contains(normalize-space(), "' + text + '")]/ancestor-or-self::' + this.selector + '[1]';
    } else {
      this.currentPath += '/descendant-or-self::*[contains(text(),"' + text + '")]/ancestor-or-self::' + this.selector + '[1]';
    }
    return this;
  }

  /**
   * This method sets the current component element to parent and selector where the export default class attribute contains the given value
   * and returns the updated component.
   * @param {string} value - Element attribute value eg : readonly
   * @returns {this} - Returns current component set by export default class attribute contains value
   */
  byClassContains(value) {
    return this.byAttributeContains('class', value);
  }

  /**
   *  This method sets the current component element to parent and selector and predicate value with given text and returns the updated component.
   * @param {string} predicate - Expects xpath
   * @returns {this} - Returns current component set by predicated value
   */
  addSelectorPredicate(predicate) {
    if (predicate.charAt(0) != '[' || predicate.charAt(predicate.length - 1) != ']') {
      throw new Error('Invalid predicate format [***] received: ' + predicate);
    }
    this.selector = this.selector + predicate;
    this.currentPath = this.parent + '//' + this.selector;
    return this;
  }

  /**
   *  This method sets the current component element by predicate attribute that contains value and returns the updated component.
   * @param {string} attribute - element attribute value eg : readonly
   * @param {string} text - Given text
   * @returns {this} - Returns current component set by predicated attribute contains value
   */
  addPredicateAttributeContains(attribute, text) {
    return this.addSelectorPredicate('[contains(@' + attribute + ', "' + text + '")]')
  }

  /**
   * This method sets the current component element by predicate export default class attribute that contains given value and returns the updated component.
   * @param {string} classValue - export default class value
   * @returns {this} - Returns current component set by predicated export default class contains value
   */
  addPredicateClassContains(classValue) {
    return this.addPredicateAttributeContains('class', classValue);
  }

  /**
   * This method sets the current component element by predicate icon attribute that contains given royavalue and returns the updated component.
   * @param {string} iconValue - icon value
   * @returns {this} - Returns current component set by predicated icon contains value
   */
  addPredicateIconContains(iconValue) {
    return this.addPredicateAttributeContains('icon', iconValue);
  }
  /**
 * This method sets the current component element by child selector and returns the updated component.
 * @param {string} childSelector - expects xpath without leading slash
 * @returns {this} - Returns current component set by child selector
 */
  addPredicadeHasDescendant(childSelector) {
    return this.addSelectorPredicate('[descendant::' + childSelector + ']')
  }
  /**
 * This method will return an xpath for an ancestor of the currentpath by a given selector like 'div[@class="red"]'
 * @param {string} selector - an xpath selector without leading slashes
 * @returns {string} - Returns xpath string of currentpath + /ancestor:: + given selector + [1]
 */
  getFirstAncestorBySelector(selector) {
    return this.currentPath + '/ancestor::' + selector + '[1]';
  }
}
