/*
The MIT License (MIT)
Copyright (c) 2014-2020 Nikolai Suslov and the Krestianstvo.org project contributors. (https://github.com/NikolaySuslov/livecodingspace/blob/master/LICENSE.md)

Virtual World Framework Apache 2.0 license  (https://github.com/NikolaySuslov/livecodingspace/blob/master/licenses/LICENSE_VWF.md)
*/

/// XPath resolution functions.
/// 
/// @module vwf/utility/xpath

class XPath {

  constructor() {
      console.log("XPath constructor");

      let self = this;

     // -- regex ----------------------------------------------------------------------------

        /// Regexes to crack the XPath string.

        this.regex = ( function() {

          var name = "[A-Za-z_][A-Za-z_0-9.-]*",              // XPath QName: http://www.w3.org/TR/xpath20/#prod-xpath-QName
              singleQuotedName = "'(?:[^'\\\\]|\\'|\\\\)+'",  // Single-quoted QName (VWF extension)
              doubleQuotedName = '"(?:[^"\\\\]|\\"|\\\\)+"',  // Double-quoted QName (VWF extension)
              wildcard = "\\*";                               // XPath Wildcard: http://www.w3.org/TR/xpath20/#prod-xpath-Wildcard

          /// @field

          var step =                                          // XPath StepExpr: http://www.w3.org/TR/xpath20/#prod-xpath-StepExpr

              "(?:" +

                "(?:" +

                  "(?:" +
                    "(?:(" + name + ")::)" +                  // "axis", as in "axis::"" (axis_name)
                  "|" +
                    "(@|)" +                                  // "@", "" (abbreviated_axis_specifier)
                  ")" +

                  "(?:" +

                    "(?:" +
                      "(" + name + ")" +                      // "kind" (node_kind)
                      "\\(" +                                 // "("
                        "(?:" +
                          "(?:" +
                            "(" + name + ")" +                // "node" (node_name)
                          "|" +
                            "(" +
                              "(?:" + singleQuotedName + ")" + // "'node'" (node_name_quoted, quoted and with internal escapes)
                            "|" +
                              "(?:" + doubleQuotedName + ")" + // "\"node\"" (node_name_quoted, quoted and with internal escapes)
                            ")" +
                          "|" +
                            "(" + wildcard + ")" +            // "*" (node_name_wildcard)
                          ")" +
                          "(?:" +
                            "," +                             // ","
                            "(?:" +
                              "(" + name + ")" +              // "type" (type_name)
                            "|" +
                              "(" +
                                "(?:" + singleQuotedName + ")" + // "'type'" (type_name_quoted, quoted and with internal escapes)
                              "|" +
                                "(?:" + doubleQuotedName + ")" + // '"type"' (type_name_quoted, quoted and with internal escapes)
                              ")" +
                            ")" +
                          ")?" +
                        ")?" +
                      "\\)" +                                 // ")"
                    ")" +

                  "|" +

                    "(?:" +
                      "(" + name + ")" +                      // "name" (name_test)
                    "|" +
                      "(" +
                        "(?:" + singleQuotedName + ")" +      // "'name'" (name_test_quoted, quoted and with internal escapes)
                      "|" +
                        "(?:" + doubleQuotedName + ")" +      // '"name"' (name_test_quoted, quoted and with internal escapes)
                      ")" +
                    "|" +
                      "(" + wildcard + ")" +                  // "*" (name_test_wildcard)
                    ")" +

                  ")" +

                ")" +

              "|" +

                "(\\.\\.|\\.)" +                             // "..", "." (abbreviated_step)

              ")";

          /// @field

          var predicate =                                     // XPath Predicate: http://www.w3.org/TR/xpath20/#prod-xpath-Predicate

              "\\[" +
                  "(" +
                      step + // "[^\\]]*" +
                  ")" +
              "\\]";

          /// @field

          var separator = "/";

          var regexes = {

              /// @field

              step: new RegExp( "^" + step ),

              /// @field

              predicate: new RegExp( "^" + predicate ),

              /// @field

              separator: new RegExp( "^" + separator ),

          };

          return regexes;

      } )()


  }

  // -- resolve --------------------------------------------------------------------------

        /// Resolve an XPath expression, using a callback function to interpret each step.
        /// 
        /// @param {String|String[]|Object[]} xpath
        /// @param {ID} rootID
        /// @param {ID|ID[]} contextIDs
        /// @param {Function} callback
        /// @param {Object} [thisArg]
        /// 
        /// @returns {ID[]|undefined}

        resolve ( xpath, rootID, contextIDs, callback /* ( step, id, resolveAttributes ) */, thisArg ) {

          // Accept contextIDs as either a single id or an array of ids.

          if ( ! ( contextIDs instanceof Array ) ) {
              contextIDs = [ contextIDs ];
          }

          // Parse the expression.

          var steps = this.parse( xpath );

          if ( steps ) {

              // Reset the context if it's an absolute path.

              if ( steps.absolute ) {
                  contextIDs = rootID ? [ rootID ] : [];
              }

              // Resolve each step.

              steps.forEach( function( step ) {

                  contextIDs = Array.prototype.concat.apply( [], contextIDs.map( function( id ) {

                      var stepIDs = callback.call( thisArg, step, id );

                      step.predicates && step.predicates.forEach( function( predicate ) {

                          stepIDs = stepIDs.filter( function( stepID ) {
                              return this.resolve( predicate, rootID, stepID, function( step, id ) {
                                  return callback.call( this, step, id, true );
                              }, thisArg ).length;
                          }, this );

                      }, this  );

                      return stepIDs;

                  }, this ) );

              }, this );

              return contextIDs;
          }

      }

              // -- parse ----------------------------------------------------------------------------

        /// Parse an XPath expression into a series of steps.
        /// 
        /// @param {String|String[]|Object[]} xpath
        /// 
        /// @returns {Object[]|undefined}

        parse ( xpath ) {

          var steps = [], step;

          if ( typeof xpath == "string" || xpath instanceof String ) {

              if ( xpath[0] == "/" ) {
                  steps.absolute = true;
                  xpath = { string: xpath, index: 1 };
              } else {
                  xpath = { string: xpath, index: 0 };
              }

              while ( xpath.index < xpath.string.length &&
                      ( step = /* assignment! */ this.parseStep( xpath ) ) ) {
                  steps.push( step );
              }

              if ( xpath.index < xpath.string.length ) {
                  return undefined;
              }

          } else if ( typeof xpath[0] == "string" || xpath[0] instanceof String ) {

              var valid = true;

              steps = xpath.map( function( step ) {

                  step = this.parseStep( step );
                  valid = valid && step;

                  return step;

              }, this );

              if ( ! valid ) {
                  return undefined;
              }

          } else {

              steps = xpath;
          }

          return steps;
      }

      // -- parseStep ------------------------------------------------------------------------

      /// Parse an XPath step expression.
      /// 
      /// @param {String|Object} xpath
      /// 
      /// @returns {Object|undefined}

      parseStep ( xpath ) {

          if ( typeof xpath == "string" || xpath instanceof String ) {
              xpath = { string: xpath, index: 0 };
          }

          if ( xpath.index < xpath.string.length && ! this.regex.separator.test( xpath.string.slice( xpath.index ) ) ) {
              var step_match = this.regex.step.exec( xpath.string.slice( xpath.index ) );
          } else {
              var step_match = [].concat( "", new Array( 11 ), "" /* abbreviated_step */ ); // special case for "//"
          }

          if ( step_match ) {

              xpath.index += step_match[0].length;

              var axis_name = step_match[1],
                  abbreviated_axis_specifier = step_match[2],
                  node_kind = step_match[3],
                  node_name = step_match[4],
                  node_name_quoted = step_match[5],
                  node_name_wildcard = step_match[6],
                  type_name = step_match[7],
                  type_name_quoted = step_match[8],
                  name_test = step_match[9],
                  name_test_quoted = step_match[10],
                  name_test_wildcard = step_match[11],
                  abbreviated_step = step_match[12];

              if ( name_test || name_test_quoted || name_test_wildcard ) {
                node_name = name_test;
                node_name_quoted = name_test_quoted;
                node_name_wildcard = name_test_wildcard;
              }

              if ( node_name_quoted ) {
                node_name = this.unquoteName( node_name_quoted );
              }

              if ( type_name_quoted ) {
                type_name = this.unquoteName( type_name_quoted );
              }

              switch ( abbreviated_step ) {

                  case "": // "" == "descendant-or-self:node()"
                      axis_name = "descendant-or-self";
                      node_kind = "node";
                      break;

                  case ".": // "." == "self::node()"
                      axis_name = "self";
                      node_kind = "node";
                      break;

                  case "..": // ".." == "parent::node()"
                      axis_name = "parent";
                      node_kind = "node";
                      break;

              }

              switch ( abbreviated_axis_specifier ) {

                  case "": // "name" == "child::name"
                      axis_name = "child";
                      break;

                  case "@": // "@name" == "attribute::name"
                      axis_name = "attribute";
                      break;

              }

              // // * == element()

              // "preceding::"
              // "preceding-sibling::"

              // "ancestor-or-self::"
              // "ancestor::"
              // "parent::"
              // "self::"
              // "child::"
              // "descendant::"
              // "descendant-or-self::"

              // "following-sibling::"
              // "following::"

              // // * == attribute()

              // "attribute::"

              // // * == namespace()

              // "namespace::"

              if ( node_name_wildcard && ! node_kind ) {

                  switch ( axis_name ) {

                      default:
                          node_kind = "element";
                          break;

                      case "attribute":
                          node_kind = "attribute";
                          break;

                      case "namespace":
                          node_kind = "namespace";
                          break;

                  }

              }

              // Parse the predicates.

              var predicates = [], predicate;

              while ( predicate = /* assignment! */ this.parsePredicate( xpath ) ) {
                  predicates.push( predicate );
              }

              // Absorb the separator.

              this.parseSeparator( xpath );

              // Now have: axis_name and name_test | node_kind(node_name,type_name)

              var step = {
                  axis: axis_name,
                  kind: node_kind,
                  name: node_name,
                  type: type_name,
              };

              if ( predicates.length ) {
                  step.predicates = predicates;
              }

              return step;
          }

      }

      // -- parsePredicate -------------------------------------------------------------------

      /// Parse an XPath step predicate.
      /// 
      /// @param {String|Object} xpath
      /// 
      /// @returns {Object[]|undefined}

      parsePredicate ( xpath ) {

          if ( typeof xpath == "string" || xpath instanceof String ) {
              xpath = { string: xpath, index: 0 };
          }

          var predicate_match = this.regex.predicate.exec( xpath.string.slice( xpath.index ) );

          if ( predicate_match ) {
              xpath.index += predicate_match[0].length;
              return this.parse( predicate_match[1] );
          }

      }

      // -- parseSeparator -------------------------------------------------------------------

      /// Parse an XPath step separator.
      /// 
      /// @param {String|Object} xpath
      /// 
      /// @returns {Boolean|undefined}

      parseSeparator ( xpath ) {

          if ( typeof xpath == "string" || xpath instanceof String ) {
              xpath = { string: xpath, index: 0 };
          }

          var separator_match = this.regex.separator.exec( xpath.string.slice( xpath.index ) );

          if ( separator_match ) {
              xpath.index += separator_match[0].length;
              return true;
          }

      }

      // -- quoteName --------------------------------------------------------------------------

      /// Apply quotation marks around a name and escape internal quotation marks and escape
      /// characters.
      /// 
      /// @param {String} unquoted_name
      /// 
      /// @returns {String}

      quoteName ( unquoted_name ) {
          return '"' + unquoted_name.replace( /(["\\])/g, "\\$1" ) + '"';
      }

      // -- unquoteName ------------------------------------------------------------------------

      /// Remove the enclosing quotation marks and unescape internal quotation marks and escape
      /// characters of a quoted name.
      /// 
      /// @param {String} quoted_name
      /// 
      /// @returns {String}

      unquoteName ( quoted_name ) {
          if ( quoted_name[0] == "'" ) {
            return quoted_name.slice( 1, -1 ).replace( /\\(['\\])/g, "$1" );
          } else if ( quoted_name[0] == '"' ) {
            return quoted_name.slice( 1, -1 ).replace( /\\(["\\])/g, "$1" );
          }
      }


}

export {
  XPath
}