/*************************************************************
 *
 *  [Contrib]/a11y/collapsible.js
 *  
 *  A filter to add maction elements to the enriched MathML for parts that
 *  can be collapsed.  We determine this based on a "complexity" value and
 *  collapse those terms that exceed a given complexity.
 *
 *  The parameters controlling the complexity measure still need work.
 *
 *  ---------------------------------------------------------------------
 *  
 *  Copyright (c) 2016-2017 The MathJax Consortium
 * 
 *  Licensed 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 CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

(function (HUB) {
  var MML;
  
  var SETTINGS = HUB.config.menuSettings;
  var COOKIE = {};   // replaced when menu is available
  
  var NOCOLLAPSE = 10000000; // really big complexity
  var COMPLEXATTR = "data-semantic-complexity";

  //
  //  Set up the a11y path,if it isn't already in place
  //
  var PATH = MathJax.Ajax.config.path;
  if (!PATH.a11y) PATH.a11y = HUB.config.root + "/extensions/a11y";

  var Collapsible = MathJax.Extension.collapsible = {
    version: "1.2.2",
    config: HUB.CombineConfig("collapsible",{
      disabled: false
    }),
    dependents: [],            // the extensions that depend on this one
    COMPLEXATTR: COMPLEXATTR,  // attribute name for the complexity value

    /*****************************************************************/

    //
    //  Complexity values to use for different structures
    //
    COMPLEXITY: {
      TEXT: .5,           // each character of a token element adds this to complexity
      TOKEN: .5,          // each toekn element gets this additional complexity
      CHILD: 1,           // child nodes add this to their parent node's complexity

      SCRIPT: .8,         // script elements reduce their complexity by this factor
      SQRT: 2,            // sqrt adds this extra complexity
      SUBSUP: 2,          // sub-sup adds this extra complexity
      UNDEROVER: 2,       // under-over adds this extra complexity
      FRACTION: 2,        // fractions add this extra complexity
      ACTION: 2,          // maction adds this extra complexity
      PHANTOM: 0,         // mphantom makes complexity 0?
      XML: 2,             // Can't really measure complexity of annotation-xml, so punt
      GLYPH: 2            // Can't really measure complexity of mglyph, to punt
    },
    //
    //  These are the cut-off complexity values for when
    //  the structure should collapse
    //
    COLLAPSE: {
      identifier: 3,
      number: 3,
      text: 10,
      infixop: 15,
      relseq: 15,
      multirel: 15,
      fenced: 18,
      bigop: 20,
      integral: 20,
      fraction: 12,
      sqrt: 9,
      root: 12,
      vector: 15,
      matrix: 15,
      cases: 15,
      superscript: 9,
      subscript: 9,
      subsup: 9,
      punctuated: {
        endpunct: NOCOLLAPSE,
        startpunct: NOCOLLAPSE,
        default: 12
      }
    },
    //
    //  These are the characters to use for the various collapsed elements
    //  (if an object, then semantic-role is used to get the character 
    //  from the object)
    //
    MARKER: {
      identifier: "x",
      number: "#",
      text: "...",
      appl: {
        "limit function": "lim",
        default: "f()"
      },
      fraction: "/",
      sqrt: "\u221A",
      root: "\u221A",
      superscript: "\u25FD\u02D9",
      subscript: "\u25FD.",
      subsup:"\u25FD:",
      vector: {
        binomial: "(:)",
        determinant: "|:|",
        default: "\u27E8:\u27E9"
      },
      matrix: {
        squarematrix: "[::]",
        rowvector: "\u27E8\u22EF\u27E9",
        columnvector: "\u27E8\u22EE\u27E9",
        determinant: "|::|",
        default: "(::)"
      },
      cases: "{:",
      infixop: {
        addition: "+",
        subtraction: "\u2212",
        multiplication: "\u22C5",
        implicit: "\u22C5",
        default: "+"
      },
      punctuated: {
        text: "...",
        default: ","
      }
    },

    /*****************************************************************/

    Enable: function (update,menu) {
      SETTINGS.collapsible = true;
      if (menu) COOKIE.collapsible = true;
      this.config.disabled = false;
      MathJax.Extension["semantic-enrich"].Enable(false,menu);
      if (update) HUB.Queue(["Reprocess",HUB]);
    },
    Disable: function (update,menu) {
      SETTINGS.collapsible = false;
      if (menu) COOKIE.collapsible = false;
      this.config.disabled = true;
      for (var i = this.dependents.length-1; i >= 0; i--) {
        var dependent = this.dependents[i];
        if (dependent.Disable) dependent.Disable(false,menu);
      }
      if (update) HUB.Queue(["Reprocess",HUB]);
    },
    
    //
    //  Register a dependent
    //
    Dependent: function (extension) {
      this.dependents.push(extension);
    },

    Startup: function () {
      MML = MathJax.ElementJax.mml;
      //
      //  Inform semantic-enrich extension that we are a dependent
      //
      var Enrich = MathJax.Extension["semantic-enrich"];
      if (Enrich) Enrich.Dependent(this);
      //
      //  Add the filter into the post-input hooks (priority 100, so other
      //  hooks run first, in particular, the enrichment hook).
      //
      HUB.postInputHooks.Add(["Filter",Collapsible],100);
    },
    
    //
    //  The main filter:  add mactions for collapsing the math.
    //
    Filter: function (jax,id,script) {
      if (!jax.enriched || this.config.disabled) return;
      jax.root = jax.root.Collapse();
      jax.root.inputID = script.id;
    },
    
    /*****************************************************************/

    //
    //  Create a marker from a given string of characters
    //  (several possibilities are commented out)
    //
//    Marker: function (c) {return MML.mtext("\u25C3"+c+"\u25B9").With({mathcolor:"blue",attr:{},attrNames:[]})},
//    Marker: function (c) {return MML.mtext("\u25B9"+c+"\u25C3").With({mathcolor:"blue",attr:{},attrNames:[]})},
    Marker: function (c) {return MML.mtext("\u25C2"+c+"\u25B8").With({mathcolor:"blue",attr:{},attrNames:[]})},
//    Marker: function (c) {return MML.mtext("\u25B8"+c+"\u25C2").With({mathcolor:"blue",attr:{},attrNames:[]})},
//    Marker: function (c) {return MML.mtext("\u27EA"+c+"\u27EB").With({mathcolor:"blue",attr:{},attrNames:[]})},



    /*****************************************************************/

    //
    //  Make a collapsible element using maction that contains
    //  an appropriate marker, and the expanded MathML.
    //  If the MathML is a <math> node, make an mrow to use instead,
    //    and move the semantic data to it (I guess it would have been
    //    easier to have had that done initially, oh well).
    //
    MakeAction: function (collapse,mml) {
      var maction = MML.maction(collapse).With({
        id:this.getActionID(), actiontype:"toggle",
        complexity:collapse.getComplexity(), collapsible:true,
        attrNames:["id","actiontype","selection",COMPLEXATTR], attr:{}, selection:2
      });
      maction.attr[COMPLEXATTR] = maction.complexity;
      if (mml.type === "math") {
        var mrow = MML.mrow().With({
          data: mml.data,
          complexity: mml.complexity,
          attrNames: [], attr: {}
        });
        for (var i = mml.attrNames.length-1, name; name = mml.attrNames[i]; i--) {
          if (name.substr(0,14) === "data-semantic-") {
            mrow.attr[name] = mml.attr[name];
            mrow.attrNames.push(name);
            delete mml.attr[name];
            mml.attrNames.splice(i,1);
          }
        }
        mrow.complexity = mml.complexity;
        maction.Append(mrow); mml.data = []; mml.Append(maction);
        mml.complexity = maction.complexity; maction = mml;
      } else {
        maction.Append(mml);
      }
      return maction;
    },
    
    actionID: 1,
    getActionID: function () {return "MJX-Collapse-"+this.actionID++},

    /*****************************************************************/

    //
    //  If there is a specific routine for the type, do that, otherwise
    //  check if there is a complexity cut-off and marker for this type.
    //  If so, check if the complexity exceeds the cut off, and
    //    collapse using the appropriate marker for the type
    //  Return the (possibly modified) MathML
    //
    Collapse: function (mml) {
      mml.getComplexity();
      var type = (mml.attr||{})["data-semantic-type"];
      if (type) {
        if (this["Collapse_"+type]) mml = (this["Collapse_"+type])(mml);
        else if (this.COLLAPSE[type] && this.MARKER[type]) {
          var role = mml.attr["data-semantic-role"];
          var complexity = this.COLLAPSE[type];
          if (typeof(complexity) !== "number") complexity = complexity[role] || complexity.default;
          if (mml.complexity > complexity) {
            var marker = this.MARKER[type];
            if (typeof(marker) !== "string") marker = marker[role] || marker.default;
            mml = this.MakeAction(this.Marker(marker),mml);
          }
        }
      }
      return mml;
    },

    //
    //  If a parent is going to be collapsible, if can call this
    //  to put back a collapsed child (rather than have too many 
    //  nested collapsings)
    //
    UncollapseChild: function (mml,n,m) {
      if (m == null) m = 1;
      if (this.SplitAttribute(mml,"children").length === m) {
        var child = (mml.data.length === 1 && mml.data[0].inferred ? mml.data[0] : mml);
        if (child && child.data[n] && child.data[n].collapsible) {
          child.SetData(n,child.data[n].data[1]);
          mml.complexity = child.complexity = null; mml.getComplexity();
          return 1;
        }
      }
      return 0;
    },
    
    //
    //  Locate child node and return its text
    //
    FindChildText: function (mml,id) {
      var child = this.FindChild(mml,id);
      return (child ? (child.CoreMO()||child).data.join("") : "?");
    },
    FindChild: function (mml,id) {
      if (mml) {
        if (mml.attr && mml.attr["data-semantic-id"] === id) return mml;
        if (!mml.isToken) {
          for (var i = 0, m = mml.data.length; i < m; i++) {
            var child = this.FindChild(mml.data[i],id);
            if (child) return child;
          }
        }
      }
      return null;
    },
    
    //
    //  Split a data attribute at commas
    //
    SplitAttribute: function (mml,id) {
      return (mml.attr["data-semantic-"+id]||"").split(/,/);
    },

    /*****************************************************************/
    /*
     *  These routines implement the collapsing of the various semantic types
     */

    //
    //  For fenced elements, if the contents are collapsed,
    //    collapse the fence instead.
    //
    Collapse_fenced: function (mml) {
      this.UncollapseChild(mml,1);
      if (mml.complexity > this.COLLAPSE.fenced) {
        if (mml.attr["data-semantic-role"] === "leftright") {
          var marker = mml.data[0].data.join("") + mml.data[mml.data.length-1].data.join("");
          mml = this.MakeAction(this.Marker(marker),mml);
        }
      }
      return mml;
    },
    
    //
    //  Collapse function applications if the argument is collapsed
    //  (Handle role="limit function" a bit better?)
    //
    Collapse_appl: function (mml) {
      if (this.UncollapseChild(mml,2,2)) {
        var marker = this.MARKER.appl;
        marker = marker[mml.attr["data-semantic-role"]] || marker.default;
        mml = this.MakeAction(this.Marker(marker),mml);
      }
      return mml;
    },

    //
    //  For sqrt elements, if the contents are collapsed,
    //    collapse the sqrt instead.
    //
    Collapse_sqrt: function (mml) {
      this.UncollapseChild(mml,0);
      if (mml.complexity > this.COLLAPSE.sqrt)
        mml = this.MakeAction(this.Marker(this.MARKER.sqrt),mml);
      return mml;
    },
    Collapse_root: function (mml) {
      this.UncollapseChild(mml,0);
      if (mml.complexity > this.COLLAPSE.sqrt)
        mml = this.MakeAction(this.Marker(this.MARKER.sqrt),mml);
      return mml;
    },

    //
    //  For enclose, include enclosure in collapsed child, if any
    //
    Collapse_enclose: function (mml) {
      if (this.SplitAttribute(mml,"children").length === 1) {
        var child = (mml.data.length === 1 && mml.data[0].inferred ? mml.data[0] : mml);
        if (child.data[0] && child.data[0].collapsible) {
          //
          //  Move menclose into the maction element
          //
          var maction = child.data[0];
          child.SetData(0,maction.data[1]);
          maction.SetData(1,mml);
          mml = maction;
        }
      }
      return mml;
    },

    //
    //  For bigops, get the character to use from the largeop at its core.
    //
    Collapse_bigop: function (mml) {
      if (mml.complexity > this.COLLAPSE.bigop || mml.data[0].type !== "mo") {
        var id = this.SplitAttribute(mml,"content").pop();
        var op = Collapsible.FindChildText(mml,id);
        mml = this.MakeAction(this.Marker(op),mml);
      }
      return mml;
    },
    Collapse_integral: function (mml) {
      if (mml.complexity > this.COLLAPSE.integral || mml.data[0].type !== "mo") {
        var id = this.SplitAttribute(mml,"content")[0];
        var op = Collapsible.FindChildText(mml,id);
        mml = this.MakeAction(this.Marker(op),mml);
      }
      return mml;
    },
    
    //
    //  For multirel and relseq, use proper symbol
    //
    Collapse_relseq: function (mml) {
      if (mml.complexity > this.COLLAPSE.relseq) {
        var content = this.SplitAttribute(mml,"content");
        var marker = Collapsible.FindChildText(mml,content[0]);
        if (content.length > 1) marker += "\u22EF";
        mml = this.MakeAction(this.Marker(marker),mml);
      }
      return mml;
    },
    Collapse_multirel: function (mml) {
      if (mml.complexity > this.COLLAPSE.multirel) {
        var content = this.SplitAttribute(mml,"content");
        var marker = Collapsible.FindChildText(mml,content[0]) + "\u22EF";
        mml = this.MakeAction(this.Marker(marker),mml);
      }
      return mml;
    },

    //
    //  Include super- and subscripts into a collapsed base
    //
    Collapse_superscript: function (mml) {
      this.UncollapseChild(mml,0,2);
      if (mml.complexity > this.COLLAPSE.superscript)
        mml = this.MakeAction(this.Marker(this.MARKER.superscript),mml);
      return mml;
    },
    Collapse_subscript: function (mml) {
      this.UncollapseChild(mml,0,2);
      if (mml.complexity > this.COLLAPSE.subscript)
        mml = this.MakeAction(this.Marker(this.MARKER.subscript),mml);
      return mml;
    },
    Collapse_subsup: function (mml) {
      this.UncollapseChild(mml,0,3);
      if (mml.complexity > this.COLLAPSE.subsup)
        mml = this.MakeAction(this.Marker(this.MARKER.subsup),mml);
      return mml;
    }
  };

  HUB.Register.StartupHook("End Extensions", function () {
    if (SETTINGS.collapsible == null) {
      SETTINGS.collapsible = !Collapsible.config.disabled;
    } else {
      Collapsible.config.disabled = !SETTINGS.collapsible;
    }
    HUB.Register.StartupHook("MathMenu Ready", function () {
      COOKIE = MathJax.Menu.cookie;
      var Switch = function(menu) {
        Collapsible[SETTINGS.collapsible ? "Enable" : "Disable"](true,true);
        MathJax.Menu.saveCookie();
      };
      var ITEM = MathJax.Menu.ITEM,
          MENU = MathJax.Menu.menu;
      var menu = ITEM.CHECKBOX(
        ['CollapsibleMath','Collapsible Math'], 'collapsible', {action: Switch}
      );
      var submenu = (MENU.FindId('Accessibility')||{}).submenu, index;
      if (submenu) {
        index = submenu.IndexOfId('CollapsibleMath');
        if (index !== null) {
          submenu.items[index] = menu;
        } else {
          submenu.items.push(ITEM.RULE(),menu);
        }
      } else {
        index = MENU.IndexOfId('About');
        MENU.items.splice(index,0,menu,ITEM.RULE());
      }
    },15);  // before explorer extension
  },15);

})(MathJax.Hub);


/*****************************************************************/
/*
 *  Add Collapse() and getComplexity() methods to the internal
 *  MathML elements, and override these in the elements that need
 *  special handling.
 */

MathJax.Ajax.Require("[a11y]/semantic-enrich.js");
MathJax.Hub.Register.StartupHook("Semantic Enrich Ready", function () {
  var MML = MathJax.ElementJax.mml,
      Collapsible = MathJax.Extension.collapsible,
      COMPLEXITY = Collapsible.COMPLEXITY,
      COMPLEXATTR = Collapsible.COMPLEXATTR;
      
  Collapsible.Startup(); // Initialize the collapsing process

  MML.mbase.Augment({
    //
    //  Just call the Collapse() method from the extension by default
    //  (but can be overridden)
    //
    Collapse: function () {return Collapsible.Collapse(this)},
    //
    //  If we don't have a cached complexity value,
    //    For token elements, just use the data length,
    //    Otherwise
    //      Add up the complexities of the (collapsed) children
    //      and add the child complexity based on the number of children
    //    Cache the complexity result
    //  return the complexity
    //
    getComplexity: function () {
      if (this.complexity == null) {
        var complexity = 0;
        if (this.isToken) {
          complexity = COMPLEXITY.TEXT * this.data.join("").length + COMPLEXITY.TOKEN;
        } else {
          for (var i = 0, m = this.data.length; i < m; i++) {
            if (this.data[i]) {
              this.SetData(i,this.data[i].Collapse());
              complexity += this.data[i].complexity;
            }
          }
          if (m > 1) complexity += m * COMPLEXITY.CHILD;
        }
        if (this.attrNames && !("complexity" in this)) this.attrNames.push(COMPLEXATTR);
        if (this.attr) this.attr[COMPLEXATTR] = complexity;
        this.complexity = complexity;
      }
      return this.complexity;
    },
    reportComplexity: function () {
      if (this.attr && this.attrNames && !(COMPLEXATTR in this.attr)) {
        this.attrNames.push(COMPLEXATTR);
        this.attr[COMPLEXATTR] = this.complexity;
      }
    }
  });

  //
  //  For fractions, scale the complexity of the parts, and add
  //  a complexity for fractions.
  //
  MML.mfrac.Augment({
    getComplexity: function () {
      if (this.complexity == null) {
        this.SUPER(arguments).getComplexity.call(this);
        this.complexity *= COMPLEXITY.SCRIPT;
        this.complexity += COMPLEXITY.FRACTION;
        this.attr[COMPLEXATTR] = this.complexity;
      }
      return this.complexity;
    }
  });
  
  //
  //  Square roots add extra complexity
  //
  MML.msqrt.Augment({
    getComplexity: function () {
      if (this.complexity == null) {
        this.SUPER(arguments).getComplexity.call(this);
        this.complexity += COMPLEXITY.SQRT;
        this.attr[COMPLEXATTR] = this.complexity;
      }
      return this.complexity;
    }
  });
  MML.mroot.Augment({
    getComplexity: function () {
      if (this.complexity == null) {
        this.SUPER(arguments).getComplexity.call(this);
        this.complexity -= (1-COMPLEXITY.SCRIPT) * this.data[1].getComplexity();
        this.complexity += COMPLEXITY.SQRT;
        this.attr[COMPLEXATTR] = this.complexity;
      }
      return this.complexity;
    }
  });
  
  //
  //  For msubsup, use the script complexity factor,
  //    take the maximum of the scripts,
  //    and add the sub-sup complexity
  //
  MML.msubsup.Augment({
    getComplexity: function () {
      if (this.complexity == null) {
        var C = 0;
        if (this.data[this.sub]) C = this.data[this.sub].getComplexity() + COMPLEXITY.CHILD;
        if (this.data[this.sup]) C = Math.max(this.data[this.sup].getComplexity(),C);
        C *= COMPLEXITY.SCRIPT;
        if (this.data[this.sub]) C += COMPLEXITY.CHILD;
        if (this.data[this.sup]) C += COMPLEXITY.CHILD;
        if (this.data[this.base]) C += this.data[this.base].getComplexity() + COMPLEXITY.CHILD;
        this.complexity = C + COMPLEXITY.SUBSUP;
        this.reportComplexity();
      }
      return this.complexity;
    }
  });

  //
  //  For munderover, use the script complexity factor,
  //    take the maximum of the scripts and the base,
  //    and add the under-over complexity
  //
  MML.munderover.Augment({
    getComplexity: function () {
      if (this.complexity == null) {
        var C = 0;
        if (this.data[this.sub]) C = this.data[this.sub].getComplexity() + COMPLEXITY.CHILD;
        if (this.data[this.sup]) C = Math.max(this.data[this.sup].getComplexity(),C);
        C *= COMPLEXITY.SCRIPT;
        if (this.data[this.base]) C = Math.max(this.data[this.base].getComplexity(),C);
        if (this.data[this.sub])  C += COMPLEXITY.CHILD;
        if (this.data[this.sup])  C += COMPLEXITY.CHILD;
        if (this.data[this.base]) C += COMPLEXITY.CHILD;
        this.complexity = C + COMPLEXITY.UNDEROVER;
        this.reportComplexity();
      }
      return this.complexity;
    }
  });
  
  //
  //  For mphantom, complexity is 0?
  //
  MML.mphantom.Augment({
    getComplexity: function () {
      this.complexity = COMPLEXITY.PHANTOM;
      this.reportComplexity();
      return this.complexity;
    }
  });
  
  //
  //  For ms, add width of quotes.  Don't cache the result, since
  //  mstyle above it could affect the result.
  //
  MML.ms.Augment({
    getComplexity: function () {
      this.SUPER(arguments).getComplexity.call(this);
      this.complexity += this.Get("lquote").length * COMPLEXITY.TEXT;
      this.complexity += this.Get("rquote").length * COMPLEXITY.TEXT;
      this.attr[COMPLEXATTR] = this.complexity;
      return this.complexity;
    }
  });

// ### FIXME:  getComplexity special cases: 
//             mtable, mfenced, mmultiscript

  //
  //  For menclose, complexity goes up by a fixed amount
  //
  MML.menclose.Augment({
    getComplexity: function () {
      if (this.complexity == null) {
        this.SUPER(arguments).getComplexity.call(this);
        this.complexity += COMPLEXITY.ACTION;
        this.attr[COMPLEXATTR] = this.complexity;
      }
      return this.complexity;
    }
  });

  //
  //  For maction, complexity is complexity of selected element
  //
  MML.maction.Augment({
    getComplexity: function () {
      //
      //  Don't cache it, since selection can change.
      //
      this.complexity = (this.collapsible ? this.data[0] : this.selected()).getComplexity();
      this.reportComplexity();
      return this.complexity;
    }
  });

  //
  //  For semantics, complexity is complexity of first child
  //
  MML.semantics.Augment({
    getComplexity: function () {
      if (this.complexity == null) {
        this.complexity = (this.data[0] ? this.data[0].getComplexity() : 0);
        this.reportComplexity();
      }
      return this.complexity;
    }
  });

  //
  //  Use fixed complexity, since we can't really measure it
  //
  MML["annotation-xml"].Augment({
    getComplexity: function () {
      this.complexity = COMPLEXITY.XML;
      this.reportComplexity();
      return this.complexity;
    }
  });
  MML.annotation.Augment({
    getComplexity: function () {
      this.complexity = COMPLEXITY.XML;
      this.reportComplexity();
      return this.complexity;
    }
  });

  //
  //  Use fixed complexity, since we can't really measure it
  //
  MML.mglyph.Augment({
    getComplexity: function () {
      this.complexity = COMPLEXITY.GLYPH;
      this.reportComplexity();
      return this.complexity;
    }
  });

  //
  //  Signal that we are ready
  //
  MathJax.Hub.Startup.signal.Post("Collapsible Ready");
  MathJax.Ajax.loadComplete("[a11y]/collapsible.js");
});

