/*
  Copyright (c) 2023-2024 Will Rudolph <@willrudolph.com>

  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

import type {ISdjHost, ISdjSearch} from "./global-interfaces.js";
import type {DataJI, EntitySearch, FuncStrNumVoid, ItemSearch, JIValue, NumKeyStore} from "../core/interfaces.js";
import type {IDataSdj, IDescriptionSdj, IEntitySdj, IItemSdj, IJsonSdj} from "../classes/class-interfaces.js";
import {each, find, isEqual, isNull, isString, isUndefined} from "lodash-es";
import {validIntArray} from "../core/sdj-types.js";
import {ESDJ_LIMIT, SD_IDX} from "../core/statics.js";
import {SdjDescription} from "../classes/description.js";
import {getFromCoreArray} from "../util/func.std.js";

export class SdjSearch implements ISdjSearch {
  log: FuncStrNumVoid;

  private _host: ISdjHost;
  constructor(private host: ISdjHost) {
    this._host = host;
    this.log = this._host.getLogFunc("SdjSearch");
  }

  changeDataInfo(sdJson: IJsonSdj, newValue: boolean) {


    this.log(sdJson.sdInfo.name + ":" + newValue);
  } 
    
  // Excepts either / or .
  // clears or ignores first /.
  dataByPath(sdJson: IJsonSdj, dataPath: string): IDataSdj | undefined {
    if (!dataPath || !isString(dataPath) || dataPath === "." || dataPath === "/") {
      return undefined;
    }
    let routePath = (dataPath.indexOf(".") !== -1) ? dataPath.replace(/\./g, "/"): dataPath,
      routeSplit: string[] = routePath.split("/"),
      parentPath: string;

    while(routeSplit[0] === "" || routeSplit[0] === "/") {
      routeSplit.shift();
    }
    if (routeSplit.length === 0) {
      this.log(`Error finding sdKey: ${dataPath}`, 3);
      return undefined;
    }

    let rtnData: IDataSdj | undefined = find(sdJson.data, {sdKey: String(routeSplit[0])});
    if (!rtnData) {
      this.log(`Error finding sdKey:${routeSplit[0]}`, 3);
      return undefined;
    } else if (rtnData && routeSplit.length === 1) {
      return rtnData;
    }

    parentPath = String(routeSplit[0]);
    routeSplit.shift();
    while (routeSplit.length > 0) {
      if (rtnData) {
        rtnData = find(rtnData.sdChildren, {sdKey: String(routeSplit[0])});
      }
      if (rtnData) {
        parentPath += ("." + routeSplit[0]);
        routeSplit.shift();
      } else {
        this.log(`On ${parentPath}; Error finding ${routeSplit[0]}`, 3);
        return undefined;
      }
    }

    return rtnData;
  }

  /*  searchDescriptions(search: DescriptionSearch): IDescriptionSdj[] {
    let rtnArray: IDescriptionSdj[] = [],
      singleDesc: IDescriptionSdj | undefined;
    // TODO: Make more robust
    if (search.name && isString(search.name)) {
      singleDesc = find(this._host.descriptions, {name: search.name});
      if (singleDesc) {
        rtnArray.push(singleDesc);
      }
    }
    return rtnArray;
  }*/

  // Both of these searches are single prop search term/item cut-out searches
  // Adding multiple terms will be ignored after the first is found in below order
  // Unclear if multiple item search should be supported
  // but that could be achieved by external methodologies
  searchEntities(sdjDescript: IDescriptionSdj, searchEnt: EntitySearch): IEntitySdj[] {
    // TODO: Condense/split function
    const asDescSdj: SdjDescription = <SdjDescription>sdjDescript,
      rtnAry: IEntitySdj[] = [],
      pushIfAvail = (ent: IEntitySdj | undefined) => {
        if (ent) {
          rtnAry.push(ent);
        }
      };

    if (searchEnt) {
      if (searchEnt.sdId) {
        pushIfAvail(<IEntitySdj>getFromCoreArray(searchEnt.sdId, asDescSdj.graph));
      } else if (searchEnt.sdKey) {
        pushIfAvail(<IEntitySdj>getFromCoreArray(searchEnt.sdKey, asDescSdj.graph));
      } else if (searchEnt.limiter) {
        each(asDescSdj.graph, (sdjEnt: IEntitySdj) => {
          if (sdjEnt.limiter === searchEnt.limiter) {
            rtnAry.push(sdjEnt);
          }
        });
      } else if (validIntArray(searchEnt.parentIds)) {
        each(searchEnt.parentIds, (sdId: number) => {
          each(asDescSdj.graph, (entRef: IEntitySdj) => {
            if (entRef.parentIds.indexOf(sdId) !== -1) {
              rtnAry.push(entRef);
            }
          });
        });
      } else if (validIntArray(searchEnt.extendIds)) {
        each(searchEnt.extendIds, (sdId: number) => {
          each(asDescSdj.graph, (entRef: IEntitySdj) => {
            if (entRef.extendIds && entRef.extendIds.indexOf(sdId) !== -1) {
              rtnAry.push(entRef);
            }
          });
        });
      } else if (validIntArray(searchEnt.sdItems)) {
        each(searchEnt.sdItems, (sdId: number) => {
          each(asDescSdj.graph, (entRef: IEntitySdj) => {
            if (entRef.sdItems.indexOf(sdId) !== -1) {
              rtnAry.push(entRef);
            }
          });
        });
      } else if (validIntArray(searchEnt.childIds)) {
        each(searchEnt.childIds, (sdId: number) => {
          each(asDescSdj.graph, (entRef: IEntitySdj) => {
            if (entRef.sdId === 0) {
              return; 
            }
            if (entRef.childIds && entRef.childIds?.indexOf(sdId) !== -1) {
              rtnAry.push(entRef);
            }
          });
        });
      } else if (searchEnt.sdProps) {
        each(asDescSdj.graph, (entRef: IEntitySdj) => {
          if (entRef.sdId === 0) {
            return; 
          }
          if (searchEnt.checkData) {
            if (entRef.sdProps && isEqual(entRef.sdProps, searchEnt.sdProps)) {
              rtnAry.push(entRef);
            }
          } else {
            let allValid = true;
            each(searchEnt.sdProps, (propValue, key) => {
              if (entRef.sdProps && entRef.sdProps[key] !== propValue) {
                allValid = false;
                return false;
              }
              return true;
            });
            if (allValid) {
              rtnAry.push(entRef);
            }
          } 
        });
      }
    }
    return rtnAry;
  }

  searchItems(sdjDescript: IDescriptionSdj, searchItem: ItemSearch): IItemSdj[] {
    const asDescript: SdjDescription = <SdjDescription>sdjDescript,
      rtnAry: IItemSdj[] = [],
      pushIfAvail = (item: IItemSdj | undefined) => {
        if (item) {
          rtnAry.push(item);
        }
      };
    if (searchItem) {
      if (searchItem.sdId) {
        pushIfAvail(<IItemSdj>getFromCoreArray(searchItem.sdId, asDescript.items));
      } else if (searchItem.sdKey) {
        pushIfAvail(<IItemSdj>getFromCoreArray(searchItem.sdKey, asDescript.items));
      } else if (searchItem.type) {
        each(asDescript.items, (sdjItem: IItemSdj) => {
          if (sdjItem.type === searchItem.type) {
            rtnAry.push(sdjItem);
          }
        });
      } else if (searchItem.limiter) {
        each(asDescript.items, (sdjItem: IItemSdj) => {
          if (sdjItem.limiter === searchItem.limiter) {
            rtnAry.push(sdjItem);
          }
        });
      }
    }
    return rtnAry;
  }

  dataByEntity(sdJson: IJsonSdj, searchEnt: EntitySearch, dataPath?: string): IDataSdj[] {
    const dataPathItem: IDataSdj | undefined = (dataPath) ? sdJson.dataByPath(dataPath) : undefined,
      data: IDataSdj[] = (dataPath && dataPathItem) ? [dataPathItem] : sdJson.data,
      foundEnts: IEntitySdj[] = sdJson.description.searchEntities(searchEnt),
      rtnAry: IDataSdj[] = [],
      subChildSearch = (children: IDataSdj[]) => {
        each(children, (dataObj: IDataSdj) => {
          const matchedEnt = find(foundEnts, {sdId: dataObj.sdId});
          if (dataObj.entity && matchedEnt) {
            if (!searchEnt.checkData) {
              rtnAry.push(dataObj);
            } else if (searchEnt.checkData && this.checkEntityData(searchEnt, dataObj, matchedEnt)){
              rtnAry.push(dataObj);
            }
          }
          if (dataObj.sdChildren && dataObj.sdChildren?.length > 0) {
            subChildSearch(dataObj.sdChildren);
          }
        });
      };

    if (data.length > 0) {
      subChildSearch(data);
      this.log("dataByEntity;" + foundEnts.length);
    }
    return rtnAry;
  }
  dataByItem(sdJson: IJsonSdj, searchItem: ItemSearch, dataPath?: string): IDataSdj[] {
    const dataPathItem: IDataSdj | undefined = (dataPath) ? sdJson.dataByPath(dataPath) : undefined,
      data: IDataSdj[] = (dataPath && dataPathItem) ? [dataPathItem] : sdJson.data,
      foundItems: IItemSdj[] = sdJson.description.searchItems(searchItem),
      rtnAry: IDataSdj[] = [],
      subChildSearch = (children: IDataSdj[]) => {
        each(children, (dataObj: IDataSdj) => {
          each(foundItems, (item: IItemSdj): boolean => {
            if (dataObj.entity && dataObj.entity.itemRefs[item.sdKey]) {
              rtnAry.push(dataObj);
              return false;
            } else {
              return true;
            }
          });
          if (dataObj.sdChildren && dataObj.sdChildren?.length > 0) {
            subChildSearch(dataObj.sdChildren);
          }
        });
      };

    if (data.length > 0) {
      subChildSearch(data);
      this.log("dataByEntity;" + foundItems.length);
    }
    return rtnAry;
  }

  validData(entity: IEntitySdj, dataSdj: DataJI, strict = false): boolean {
    let rtnVal = true,
      errorResults: string[] = this.findDataErrors(entity, dataSdj),
      splits: string[];

    each(errorResults, (errSptStr) => {
      splits = errSptStr.split(",");
      this.log(
        `SdjEntity '${entity.sdKey}' struct '${splits[0]}' is '${splits[1]}' should be type '${splits[2]}'`
        , 3);
      rtnVal = false;
    });

    if (strict && errorResults?.length > 0) {
      const singleErr = errorResults.shift();
      if (singleErr) {
        splits = singleErr.split(",");
        throw new Error(
          `SdjData key '${entity.sdKey}' validate item '${splits[0]}' is '${splits[1]}' should be type '${splits[2]}'`);
      }
    }

    return rtnVal;
  }

  validStruct(entity: IEntitySdj, dataSdj: DataJI, parentRef: DataJI | undefined, strict = false): boolean {
    let rtnVal = true,
      errorResults: string[] = this.findStructErrors(entity, dataSdj, parentRef),
      splits: string[];

    each(errorResults, (errSptStr) => {
      splits = errSptStr.split(",");
      this.log(
        `Entity '${entity.sdKey}' with data '${splits[0]}' error '${splits[1]}' with data/ent '${splits[2]}'`
        , 3);
      rtnVal = false;
    });

    if (strict && errorResults?.length > 0) {
      const singleErr = errorResults.shift();
      if (singleErr) {
        splits = singleErr.split(",");
        throw new Error(
          `[SDJ] Entity '${entity.sdKey}' with data '${splits[0]}' error '${splits[1]}' with data/ent '${splits[2]}'`);
      }
    }

    return rtnVal;
  }

  /*    getRefByType<Type>(sdjDescript: IDescriptionSdj, idOrKey: number | string, type: string): Type | undefined {
        if (isUndefined(idOrKey) || (!isString(idOrKey) && !isNumber(idOrKey))) return undefined;
        const searchGroup = (type === "entity") ? sdjDescript.graph : sdjDescript.items;
        return (isNumber(idOrKey)) ? <Type>find(searchGroup, {sdId: idOrKey}) : <Type>find(searchGroup, {sdKey: idOrKey});
    }*/

  private findDataErrors(entity: IEntitySdj, dataJI: DataJI): string[] {
    let rtnErrStrs: string[] = [];
    each(entity.itemRefs, (itemRef: IItemSdj) => {
      const key = itemRef.sdKey,
        possibleValue = dataJI[key],
        isPresent: boolean = (!isNull(possibleValue) && !isUndefined(possibleValue)),
        actualValue: JIValue = isPresent ? <JIValue>possibleValue : false,
        isRequired = (itemRef.limiter === ESDJ_LIMIT.SYS_REQ
                    || itemRef.limiter === ESDJ_LIMIT.REQ || itemRef.limiter === ESDJ_LIMIT.REQ_HIDE),
        isValid = (isPresent) ? itemRef.validator.valid(actualValue) : false;

      if (isRequired && !isPresent) {
        rtnErrStrs.push(key + ",required," + itemRef.type);
      } else if (isRequired && isPresent && !isValid) {
        rtnErrStrs.push(key + ",invalid," + itemRef.type);
      } else if (!isRequired && isPresent && !isValid) {
        rtnErrStrs.push(key + ",invalid," + itemRef.type);
      }
    });
    return rtnErrStrs;
  }
  private findStructErrors(entity: IEntitySdj, dataJI: DataJI, parentData: DataJI | undefined): string[] {
    // TODO: Condense split function
    if (!entity.description) {
      return [entity.sdKey + ",description,not available"]; 
    }
    let rtnErrs: string[] = [],
      childPresent: NumKeyStore<number> = {},
      parentEnt: IEntitySdj | undefined = (parentData) ? entity.description.getEntity(parentData.sdId)
        : undefined,
      checkIndexing: boolean = (parentEnt?.limiter === ESDJ_LIMIT.KEY_IDX);

    each(entity.childRefs, (childRef) => {
      childPresent[childRef.sdId] = 0;
    });

    if (!parentData && entity.parentIds.indexOf(0) === -1) {
      rtnErrs.push(entity.sdKey + ",requires parent,parent undefined");
    } else if (parentEnt && entity.parentIds.indexOf(parentEnt.sdId) === -1) {
      rtnErrs.push(entity.sdKey + ",entity does not allow parent," + parentEnt.sdKey);
    } else if (parentEnt && (!parentEnt.childIds || parentEnt.childIds.indexOf(entity.sdId) === -1)) {
      rtnErrs.push(parentEnt.sdKey + ",parent does not allow child," + entity.sdKey);
    }

    each(dataJI.sdChildren, (childItem: DataJI, idx: number) => {
      const entRef = entity.childRefs[childItem.sdId];
      if (entity.childIds?.indexOf(childItem.sdId) === -1) {
        rtnErrs.push(entity.sdKey +",doesn't allow child," + childItem.sdId);
      }
      if (entRef) {
        if (entRef.limiter !== ESDJ_LIMIT.NONE) {
          childPresent[childItem.sdId] += 1;
        }
      } else {
        rtnErrs.push(entity.sdKey + ",unknown child entity," + childItem.sdKey);
      }
      if (checkIndexing && childItem.sdKey !== (SD_IDX + idx)) {
        rtnErrs.push(dataJI.sdKey + ",KEY_IDX out of order or wrong," + childItem.sdKey + "!=" + SD_IDX + idx);
      }
    });

    each(childPresent, (totals, sdId) => {
      const childEnt = entity.childRefs[sdId];
      if (childEnt) {
        switch(childEnt.limiter) {
        case ESDJ_LIMIT.REQ:
          if (totals === 0) {
            rtnErrs.push(entity.sdKey + ",missing req'd," + childEnt.sdKey);
          }
          break;
        case ESDJ_LIMIT.REQ_HIDE:
          if (totals !== 1) {
            rtnErrs.push(entity.sdKey + `,single req hidden 1 != '${totals}',` + childEnt.sdKey);
          }
          break;
        case ESDJ_LIMIT.ONE_NONE:
          if (totals > 1) {
            rtnErrs.push(entity.sdKey + ",can only have one or none," + childEnt.sdKey);
          }
          break;
        case ESDJ_LIMIT.REQ_ONE:
          if (totals > 1 || totals === 0) {
            rtnErrs.push(entity.sdKey + ",requires one single," + childEnt.sdKey);
          }
          break;
        default:
            // default would be NONE/SD_IDX
        }
      }

    });

    return rtnErrs;
  }


  private checkEntityData(searchEnt: EntitySearch, inData: IDataSdj, checkEnt: IEntitySdj): boolean {
    let rtnVal = false;
    // Assume single value array for now
    if (searchEnt.childIds) {
      if (find(inData.sdChildren, {sdId: checkEnt.sdId})) {
        rtnVal = true;
      }
    } else if (searchEnt.parentIds) {
      if (inData.parentRef && inData.parentRef.sdId === checkEnt.sdId) {
        rtnVal = true;
      }
    } else if (searchEnt.sdItems) {
      const checksdId = searchEnt.sdItems[0],
        foundItem = (checksdId) ? find(checkEnt.itemRefs, {sdId: checksdId}) : undefined;
      if (foundItem) {
        rtnVal = Boolean(!isUndefined(inData.getDataKey(foundItem.sdKey)));
      }
    } else {
      rtnVal = true;
    }

    return rtnVal;
  }
}
