/**
 * 获取标签css selector
 * @param res 
 * @ignoreId 是否忽略id选择器
 * @returns 
 */
export function getSelector(res:HTMLElement,ignoreId=false){
  if(!res||!res.tagName)return '';
  let tagName=res.tagName.toLowerCase();
  let id=res.id
  let clazz=res.getAttribute('class')||''
  let selector:string[]=[]
  clazz.trim().split(' ').forEach(o=>{
    if(o.indexOf('active')==-1&&o.trim()!=''){
      selector.push(o)
    }
  })
  if(id&&!ignoreId){
    return tagName+'#'+id
  }else if(clazz){
    return (tagName+'.'+selector.join('.'))//.toLowerCase();
  }else{//没有任何标识的时候只能取上级
    let index=prevIndex(res)
    let pSelector=parentSelector(res,ignoreId)
    let ppSelector=''
    if(index==-1){
      ppSelector=parentSelector(res.parentElement,ignoreId)
    }
    return (ppSelector?(ppSelector+'>'):'')+pSelector+'>'+tagName+(index==-1?'':`:nth-child(${index})`)
    // return pSelector+'>'+tagName+(index==-1?'':`:nth-child(${index})`)
    // return pSelector+'>'+tagName+`:eq(${index})`
  }
}
/**
 * 获取当前元素排行位置
 * @param element 
 * @returns 
 */
function prevIndex(element:HTMLElement) {//获取当前节点的上一个元素节点
  let index=1;
  let el=element.previousSibling
  
  do {
    if (el&&el.nodeType !=3){  
      index++; 
    }
    if(el){
      el = el.previousSibling;
    }
  } while (el);
  let flag=false;//有同级别一样的元素时需要
  let children=element.parentNode?.childNodes||[]
  let pos=0;
  for(let i=0;i<children.length;i++){
    let c=children[i]
    if(c.nodeType!=3){
      pos++
    }
    if(c.tagName==element.tagName&&(pos!=index)){
      flag=true;
    }
  }
  return flag?index:-1;
}
/**
 * 获取上级selector
 * @param element 
 * @returns 
 */
export function parentSelector(element:HTMLElement,ignoreId=false) {
  let p=element
  if(!p)return ''
  let selector='';
  do {
    p = p.parentNode
    if(p){
      selector=getSelector(p,ignoreId)
    }
  } while (!selector&&p);
  return selector;
}
/**
 * 获取所有上级节点的selector
 * @param {*} element 
 * @returns 
 */
export function parentsSelector(element:HTMLElement,ignoreId=false) {
  let p=element
  let selectors:string[]=[]
  do {
    let selector='';
    p = p.parentNode
    if(p){
      selector=getSelector(p,ignoreId)
      if(selector){
        selectors.push(selector)
      }
    }
  } while (p);
  return selectors;
}

export type Options = {
  ignoreId: boolean
};

const defaultOptions: Options = {
  ignoreId: false
};
/**
 * 获取xpath
 * @param el 
 * @param customOptions 
 * @returns 
 */
export function getXPath( el: any, customOptions?: Partial< Options > ): string {
  const options = { ...defaultOptions, ...customOptions };
  let nodeElem = el;
  if ( nodeElem && nodeElem.id && ! options.ignoreId ) {
      return "//*[@id=\"" + nodeElem.id + "\"]";
  }
  let parts: string[] = [];
  while ( nodeElem && ( 1 === nodeElem.nodeType || 3 === nodeElem.nodeType ) ) {
      let numberOfPreviousSiblings = 0;
      let hasNextSiblings = false;
      let sibling = nodeElem.previousSibling;
      while ( sibling ) {
          if ( sibling.nodeType !== 10 &&
              sibling.nodeName === nodeElem.nodeName
          ) {
              numberOfPreviousSiblings++;
          }
          sibling = sibling.previousSibling;
      }
      sibling = nodeElem.nextSibling;
      while ( sibling ) {
          if ( sibling.nodeName === nodeElem.nodeName ) {
              hasNextSiblings = true;
              break;
          }
          sibling = sibling.nextSibling;
      }
      let prefix = nodeElem.prefix ? nodeElem.prefix + ":" : "";
      let nth = numberOfPreviousSiblings || hasNextSiblings
          ? "[" + ( numberOfPreviousSiblings + 1 ) + "]"
          : "";
      let piece = ( nodeElem.nodeType != 3 )
          ? prefix + nodeElem.localName + nth
          : 'text()' + ( nth || '[1]' );

      parts.push( piece );
      nodeElem = nodeElem.parentNode;
      if ( nodeElem && nodeElem.id && ! options.ignoreId ) {
        parts.push("/*[@id=\"" + nodeElem.id + "\"]")
        break;
      }
  }
  return parts.length ? "/" + parts.reverse().join( "/" ) : "";
}

const isValidXPath = (expr:string) => (
  typeof expr != 'undefined' &&
  expr.replace(/[\s-_=]/g,'') !== '' &&
  expr.length === expr.replace(/[-_\w:.]+\(\)\s*=|=\s*[-_\w:.]+\(\)|\sor\s|\sand\s|\[(?:[^\/\]]+[\/\[]\/?.+)+\]|starts-with\(|\[.*last\(\)\s*[-\+<>=].+\]|number\(\)|not\(|count\(|text\(|first\(|normalize-space|[^\/]following-sibling|concat\(|descendant::|parent::|self::|child::|/gi,'').length
);

const getValidationRegex = () => {
  let regex =
      "(?P<node>"+
        "("+
          "^id\\([\"\\']?(?P<idvalue>%(value)s)[\"\\']?\\)"+// special case! `id(idValue)`
        "|"+
          "(?P<nav>//?(?:following-sibling::)?)(?P<tag>%(tag)s)" + //  `//div`
          "(\\[("+
            "(?P<matched>(?P<mattr>@?%(attribute)s=[\"\\'](?P<mvalue>%(value)s))[\"\\']"+ // `[@id="well"]` supported and `[text()="yes"]` is not
          "|"+
            "(?P<contained>contains\\((?P<cattr>@?%(attribute)s,\\s*[\"\\'](?P<cvalue>%(value)s)[\"\\']\\))"+// `[contains(@id, "bleh")]` supported and `[contains(text(), "some")]` is not
          ")\\])?"+
          "(\\[\\s*(?P<nth>\\d+|last\\(\\s*\\))\\s*\\])?"+
        ")"+
      ")";

  const subRegexes = {
      "tag": "([a-zA-Z][a-zA-Z0-9:-]*|\\*)",
      "attribute": "[.a-zA-Z_:][-\\w:.]*(\\(\\))?)",
      "value": "\\s*[\\w/:][-/\\w\\s,:;.]*"
  };

  Object.keys(subRegexes).forEach(key => {
      regex = regex.replace(new RegExp('%\\(' + key + '\\)s', 'gi'), subRegexes[key]);
  });

  regex = regex.replace(/\?P<node>|\?P<idvalue>|\?P<nav>|\?P<tag>|\?P<matched>|\?P<mattr>|\?P<mvalue>|\?P<contained>|\?P<cattr>|\?P<cvalue>|\?P<nth>/gi, '');

  return new RegExp(regex, 'gi');
};

function preParseXpath (expr:string){
  return expr.replace(/contains\s*\(\s*concat\(["']\s+["']\s*,\s*@class\s*,\s*["']\s+["']\)\s*,\s*["']\s+([a-zA-Z0-9-_]+)\s+["']\)/gi, '@class="$1"')
}
/**
 * 将xpath转成css selector
 * @param expr 
 * @returns 
 */
export function xPathToCss(expr:string) {
  if (!expr) {
      throw new Error('Missing XPath expression');
  }
  expr = preParseXpath(expr);
  if (!isValidXPath(expr)) {
      throw new Error('Invalid or unsupported XPath: ' + expr);
  }
  const xPathArr = expr.split('|');
  const prog = getValidationRegex();
  const cssSelectors = [];
  let xindex = 0;
  while (xPathArr[xindex]) {
      const css = [];
      let position = 0;
      let nodes;
      while (nodes = prog.exec(xPathArr[xindex])) {
          let attr;

          if (!nodes && position === 0) {
              throw new Error('Invalid or unsupported XPath: ' + expr);
          }
          const match = {
              node: nodes[5],
              idvalue: nodes[12] || nodes[3],
              nav: nodes[4],
              tag: nodes[5],
              matched: nodes[7],
              mattr: nodes[10] || nodes[14],
              mvalue: nodes[12] || nodes[16],
              contained: nodes[13],
              cattr: nodes[14],
              cvalue: nodes[16],
              nth: nodes[18]
          };
          let nav = '';
          if (position != 0 && match['nav']) {
              if (~match['nav'].indexOf('following-sibling::')) {
                  nav = ' + ';
              } else {
                  nav = (match['nav'] == '//') ? ' ' : ' > ';
              }
          }
          const tag = (match['tag'] === '*') ? '' : (match['tag'] || '');
          if (match['contained']) {
              if (match['cattr'].indexOf('@') === 0) {
                  attr = '[' + match['cattr'].replace(/^@/, '') + '*="' + match['cvalue'] + '"]';
              } else {
                  throw new Error('Invalid or unsupported XPath attribute: ' + match['cattr']);
              }
          } else if (match['matched']) {
              switch (match['mattr']) {
                  case '@id':
                      attr = '#' + match['mvalue'].replace(/^\s+|\s+$/,'').replace(/\s/g, '#');
                      break;
                  case '@class':
                      attr = '.' + match['mvalue'].replace(/^\s+|\s+$/,'').replace(/\s/g, '.');
                      break;
                  case 'text()':
                  case '.':
                      throw new Error('Invalid or unsupported XPath attribute: ' + match['mattr']);
                  default:
                      if (match['mattr'].indexOf('@') !== 0) {
                          throw new Error('Invalid or unsupported XPath attribute: ' + match['mattr']);
                      }
                      if (match['mvalue'].indexOf(' ') !== -1) {
                          match['mvalue'] = '\"' + match['mvalue'].replace(/^\s+|\s+$/,'') + '\"';
                      }
                      attr = '[' + match['mattr'].replace('@', '') + '="' + match['mvalue'] + '"]';
                      break;
              }
          } else if (match['idvalue']) {
              attr = '#' + match['idvalue'].replace(/\s/, '#');
          } else {
              attr = '';
          }
          let nth = '';
          if (match['nth']) {
              if (match['nth'].indexOf('last') === -1) {
                  if (isNaN(parseInt(match['nth'], 10))) {
                      throw new Error('Invalid or unsupported XPath attribute: ' + match['nth']);
                  }
                  nth = parseInt(match['nth'], 10) !== 1 ? ':nth-of-type(' + match['nth'] + ')' : ':first-of-type';
              } else {
                  nth = ':last-of-type';
              }
          }
          css.push(nav + tag + attr + nth);
          position++;
      }
      const result = css.join('');
      if (result === '') {
          throw new Error('Invalid or unsupported XPath');
      }
      cssSelectors.push(result);
      xindex++;
  }
  return cssSelectors.join(', ');
};
