import { SelectAllValue } from "../components/builder-section/groupwrapper/criteriagroup/edit-components/EditComponents";
import ObjectPropertyGroup from "../components/builder-section/groupwrapper/criteriagroup/objectpropertygroup/ObjectPropertyGroup";
import { OptionTypes } from "../components/builder-section/groupwrapper/criteriagroup/optionsgroup/OptionsGroup";
import ClassTypeId from "../components/builder-section/groupwrapper/criteriagroup/startendclassgroup/ClassTypeId";
import EndClassGroup from "../components/builder-section/groupwrapper/criteriagroup/startendclassgroup/EndClassGroup";
import StartClassGroup from "../components/builder-section/groupwrapper/criteriagroup/startendclassgroup/StartClassGroup";
import GroupWrapper from "../components/builder-section/groupwrapper/GroupWrapper";
import NoOrderBtn from "../components/buttons/NoOrderBtn";
import { SelectedVal } from "../components/SelectedVal";
import SparnaturalComponent from "../components/SparnaturalComponent";
import { WidgetValue } from "../components/widgets/AbstractWidget";
import { Branch, ISparJson, Order, VariableTerm } from "../generators/json/ISparJson";


export default class QueryLoader{
    static sparnatural: SparnaturalComponent;
    static query: ISparJson
    
    static loadQuery(query:ISparJson){
        this.query = query
        // set Sparnatural quiet so it does not emit the update callbacks
        this.sparnatural.setQuiet(true);
        // first reset the current query
        this.sparnatural.BgWrapper.resetCallback();
        // build Sparnatural query
        // use a deep copy of the query to avoid modifying the original copy
        let clone = JSON.parse(JSON.stringify(query)) as ISparJson;
        let varMapping = this.#buildSparnatural(this.sparnatural, clone.branches);
        // set the correct variable names
        this.#updateNamingOfVariables(varMapping)
        // set the correct ordering of the draggables
        this.#updateOrderingOfVariables()
        // then reset the quiet flag
        this.sparnatural.setQuiet(false);
        // trigger query generation at then
        this.sparnatural.html[0].dispatchEvent(
          new CustomEvent("generateQuery")
        );
        this.sparnatural.html[0].dispatchEvent(
          new CustomEvent("redrawBackgroundAndLinks")
        );
    }
    
    static #buildSparnatural(sparnatural: SparnaturalComponent, branches: Array<Branch>):Map<string,string> {
        let varMapping = new Map<string,string>();
        if(branches?.length === 0) throw Error('No Branches on query detected')
        // first build the rootGroupWrapper
        let rootGrpWrapper =
        sparnatural.BgWrapper.componentsList.rootGroupWrapper;
        // build the root groupwrapper and remove from branches array
        let rootBranch = branches.shift();
        let localVarMapping = this.#buildCriteriaGroup(rootGrpWrapper, rootBranch);
        localVarMapping.forEach((value: string, key:string) => { varMapping.set(key, value); });
        
        // by default, the very first start class group will be selected
        // if the first variable is *not* selected, then unselect it
        const firstStartClassVal = { type: rootBranch.line.sType, variable: rootBranch.line.s };
        if(!QueryLoader.#hasSelectedVar(this.query.variables,firstStartClassVal.variable)) {
          // click on first eye btn to unselect it
          this.#clickOn((rootGrpWrapper.CriteriaGroup.StartClassGroup.inputSelector as ClassTypeId)?.selectViewVariableBtn?.widgetHtml)
        }

        let parent = rootGrpWrapper;
        branches.forEach((b) => {
          this.#clickOn(parent.CriteriaGroup.ActionsGroup.actions.ActionAnd.btn);
          let localVarMapping = this.#buildCriteriaGroup(parent.andSibling, b);
          localVarMapping.forEach((value: string, key:string) => { varMapping.set(key, value); });
          parent = parent.andSibling;
        });

        return varMapping;
    }
  
    static #buildCriteriaGroup(grpWarpper: GroupWrapper, branch: Branch):Map<string,string> {
      let varMapping = new Map<string,string>();
      // set StartClassVal only if there wasn't one set by the parent (e.g whereChild andSibling have it already set)
      const startClassVal = { type: branch.line.sType, variable: branch.line.s };
      if (!grpWarpper.CriteriaGroup.StartClassGroup.startClassVal.type) {
        //set StartClassGroup
        this.#setSelectedValue(
            grpWarpper.CriteriaGroup.StartClassGroup,
            branch.line.sType
        );
      }
      // also set the variable name
      // grpWarpper.CriteriaGroup.StartClassGroup.startClassVal = startClassVal;
      varMapping.set(grpWarpper.CriteriaGroup.StartClassGroup.startClassVal.variable, branch.line.s);
  
      // set EndClassGroup
      const endClassVal = { type: branch.line.oType, variable: branch.line.o };
      this.#setSelectedValue(grpWarpper.CriteriaGroup.EndClassGroup, branch.line.oType);
      // transparently set the variable name to the one in the query
      // before we click on the select button, so that the column is selected with the proper name
      // grpWarpper.CriteriaGroup.EndClassGroup.endClassVal = endClassVal;
      varMapping.set(grpWarpper.CriteriaGroup.EndClassGroup.endClassVal.variable, branch.line.o);

      //set ObjectPropertyGroup
      this.#setSelectedValue(
        grpWarpper.CriteriaGroup.ObjectPropertyGroup,
        branch.line.p
      );
    
      // set WidgetValues
      branch.line.values.forEach((v) => {
        const parsedVal: WidgetValue = grpWarpper.CriteriaGroup.EndClassGroup.editComponents.widgetWrapper.widgetComponent.parseInput(v)
        // if there are multiple values rendered, click first the 'plus' btn, to add more values
        if(grpWarpper.CriteriaGroup.endClassWidgetGroup.widgetValues.length > 0) this.#clickOn(grpWarpper.CriteriaGroup.endClassWidgetGroup.addWidgetValueBtn.html)
        grpWarpper.CriteriaGroup.EndClassGroup.editComponents.widgetWrapper.widgetComponent.triggerRenderWidgetVal(parsedVal)
      });

      // if there is no value, and no children, set an "Any" value
      if(branch.line.values.length == 0 && branch.children.length == 0) {
        grpWarpper.CriteriaGroup.EndClassGroup.editComponents.onSelectAll();
      }
    
      // trigger option state
      this.#triggerOptions(grpWarpper, branch);
    
      if (branch.children.length > 0) {
        this.#clickOn(
          grpWarpper.CriteriaGroup.EndClassGroup.editComponents.actionWhere.btn
        );
        // first child
        let localVarMapping = this.#buildCriteriaGroup(grpWarpper.whereChild, branch.children.shift());
        localVarMapping.forEach((value:string,key: string) => varMapping.set(key, value));
        // the rest of the children are AND connected
        let parent = grpWarpper.whereChild;
        branch.children.forEach((c) => {
          this.#clickOn(parent.CriteriaGroup.ActionsGroup.actions.ActionAnd.btn);
          let localVarMapping = this.#buildCriteriaGroup(parent.andSibling, c);
          localVarMapping.forEach((value:string,key: string) => varMapping.set(key, value));
          parent = parent.andSibling;
        });
      }
      
      // select if the var is viewed (eye btn)
      this.#setSelectViewVariableBtn(
        startClassVal,
        grpWarpper.CriteriaGroup.StartClassGroup,
        endClassVal,
        grpWarpper.CriteriaGroup.EndClassGroup
      )

      return varMapping;
  }
  
  static #triggerOptions(grpWrapper: GroupWrapper, branch: Branch) {
    if (branch.notExists && grpWrapper.optionState != OptionTypes.NOTEXISTS) {
      this.#clickOn(grpWrapper.CriteriaGroup.OptionsGroup.optionalArrow.widgetHtml);
      this.#clickOn(grpWrapper.CriteriaGroup.OptionsGroup.NotExistsComponent.html);
    }
    if (branch.optional && grpWrapper.optionState != OptionTypes.OPTIONAL) {
      this.#clickOn(grpWrapper.CriteriaGroup.OptionsGroup.optionalArrow.widgetHtml);
      this.#clickOn(grpWrapper.CriteriaGroup.OptionsGroup.OptionalComponent.html);
    }
  }
  
  // set the value for an inputSelector and trigger the corresponding event
  static #setSelectedValue(
    component: StartClassGroup | EndClassGroup | ObjectPropertyGroup,
    value: string
  ) {
    // set the values to the ClassTypeId | ObjectPropertyTypeId component
    component.inputSelector.setSelected(value) ;
    component.inputSelector.submitSelected() ;
  }

  // this method checks if the eye btn was enabled in the loaded query
  static #setSelectViewVariableBtn(
    startClassVal:SelectedVal,
    startClassComponent:StartClassGroup,
    endClassVal:SelectedVal,
    endClassComponent:EndClassGroup
  ){
    if(QueryLoader.#hasSelectedVar(this.query.variables, endClassVal.variable)) {
      // click on eye btn
      this.#clickOn((endClassComponent.inputSelector as ClassTypeId)?.selectViewVariableBtn?.widgetHtml)
    }
  }

  static #updateOrderingOfVariables(){
    const varMenu =this.sparnatural.variableSection.variableOrderMenu
    this.query.variables.forEach(v=>{
      varMenu.draggables.forEach(d=>{
        let varName;
        if("expression" in v) {
          varName = v.expression.expression.value
        } else {
          varName = v.value;
        }

        if(d.state.selectedVariable.variable === varName){
          varMenu.removeDraggableByVarName(varName)
          let newDraggable = varMenu.addDraggableComponent(d.state.selectedVariable);
          // if this was an aggregated variable, load it by calling a specific function of the draggable
          if("expression" in v) {
            newDraggable.loadAggregatedVariable(v)
          }
        }       
      })
    })

    // once variables are sorted, synchronize with the actionstore
    // this.sparnatural.actionStore.variables = this.sparnatural.variableSection.listVariables();

    const variableSortOption =this.sparnatural.variableSection.variableSortOption;
    if(this.query.order == Order.ASC) {
      variableSortOption.changeSortOrderCallBack(Order.ASC);
    } else if(this.query.order == Order.DESC) {
      variableSortOption.changeSortOrderCallBack(Order.DESC);
    } else {
      variableSortOption.changeSortOrderCallBack(Order.NOORDER);
    }
  }

  // map of the names of variables in the current query to their target name
  static #updateNamingOfVariables(varNameMapping:Map<string,string>){
    const varMenu = this.sparnatural.variableSection.variableOrderMenu
    this.query.variables.forEach(v=>{
      varMenu.draggables.forEach(d=>{
        var varToConsider: VariableTerm;
        
        if("variable" in v) {
          // this is an aggregation
          // at this stage we are setting the variable name to the *original variable*
          // the final aggregated var name will be set when the draggable will be updated
          varToConsider = v.expression.expression
        } else {
          // nominal case
          varToConsider = v;
        }

        if(varNameMapping.get(d.state.selectedVariable.variable) === varToConsider.value){
          d.setVarName(varToConsider.value);
        }
      })
    })
  }

  static #hasSelectedVar(vars:ISparJson["variables"], varName:string):boolean {

    var result:boolean = false;
    vars.forEach(v => {
      if("expression" in v) {
        // this is an aggregation
        // consider the variable *being aggregated*, not the result of the aggregation
        if(v.expression.expression.value == varName) result = true;
      }
      if("value" in v) {
        if(v.value == varName) result = true;
      }
    });
    return result;
  }
  
  static #clickOn(el: JQuery<HTMLElement>) {
    el[0].dispatchEvent(new Event("click"));
  }

  static setSparnatural(sparnatural:SparnaturalComponent){
    this.sparnatural = sparnatural
  }

}