/*
  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 {
  CoreSD,
  DescriptionJI,
  EntityJI,
  EntitySearch,
  FuncStrNumVoid,
  Info,
  ItemJI,
  ItemSearch,
  SdKeyProps
} from "../core/interfaces.js";
import {checkResetInfo, verifyUniqKeys,} from "../util/verify.js";
import {genEntityJI, genInfoJI, genItemJI} from "../util/immutables.js";
import {BASE_ITEMS_JI, ESDJ_CLASS, GRAPH_ZERO, SYS_RESERVED} from "../core/statics.js";
import {cloneJI, getFromCoreArray} from "../util/func.std.js";
import {isArrayWithLen, isInfo, validLexiconArray} from "../core/validators.js";
import {restrictToAllowedKeys} from "../core/restrict.js";
import type {IDescriptionSdj, IEntitySdj, IItemSdj} from "./class-interfaces.js";
import {SdjEntity} from "./entity.js";
import {SdjItem} from "./item.js";
import type {ISdjHost, SdjJITypes} from "../global/global-interfaces.js";
import {clone, cloneDeep, each, find, isArray, isEqual, isFunction, times, uniq} from "lodash-es";
import {validSDKey} from "../core/sdj-types.js";
import {getRegEx} from "../util/regex.js";

// Descriptions created outside of new SdJson() or SdjHost.makeDescription
// using 'new SdjDescription(...)' ; will NOT automatically be added to a library.
// After initial creation they can be added as needed via library.storeDesc(...);


export class SdjDescription implements IDescriptionSdj {
  log: FuncStrNumVoid;
  private _lexicons: string[] = [];
  private _graph: IEntitySdj[] = [];
  private _sdInfo: Info;
  private _lang: string = "en";
  private _items: IItemSdj[] = [];
  private _host: ISdjHost;

  constructor(refDescJI: DescriptionJI, requiredHost: ISdjHost) {
    this._host = requiredHost;
    if (requiredHost?.checkClassInst && isFunction(requiredHost.checkClassInst)) {
      this.host.checkClassInst(requiredHost, ESDJ_CLASS.HOST, true);
      this.host.checkClassInst(refDescJI, ESDJ_CLASS.DESCRIPTION, false);
    } else {
      throw new Error("[SDJ] Improper required Host - use SdjHost.createDescription for auto provided host;");
    }
    SdjDescription.VerifyJI(refDescJI);
    let inDescJI: DescriptionJI;
    inDescJI = cloneJI(refDescJI);
    inDescJI.sdInfo = checkResetInfo(inDescJI.sdInfo);
    inDescJI = this._host.fullDescription(inDescJI);

    this._sdInfo = inDescJI.sdInfo;
    this.lang = (inDescJI.lang) ? String(inDescJI.lang).toLowerCase() : "en";
    this.log = this._host.getLogFunc("Desc:" + this._sdInfo.name);
    this.log("created");
    this.entityItemBuild(inDescJI);
  }

  // vvv Below are frozen when description/sdJson locked
  get graph(): SdjEntity[] {
    return <SdjEntity[]>this._graph;
  }

  set graph(inGraph: SdjEntity[]) {
    this._graph = inGraph;
  }

  set items(inItems: IItemSdj[]) {
    this._items = <SdjItem[]>inItems;
  }

  get items(): SdjItem[] {
    return <SdjItem[]>this._items;

  }
  get name(): string {
    return this._sdInfo.name;
  }

  set name(name: string) {
    this._sdInfo.name = name;
  }

  get sdInfo(): Info {
    return this._sdInfo;
  }

  get lang() {
    return this._lang;
  }

  set lang(inStr: string) {
    if (getRegEx("lang").test(inStr)) {
      this._lang = String(inStr).toLowerCase();
    }
  }

  // ^^ Above Frozen when locked

  get host(): ISdjHost {
    return this._host;
  }

  searchEntities(searchEnt: EntitySearch): IEntitySdj[] {
    return this._host.searchMgr.searchEntities(this, searchEnt);
  }

  searchItems(searchItem: ItemSearch): IItemSdj[] {
    return this._host.searchMgr.searchItems(this, searchItem);
  }

  getEntity (entKeyNum: number | string): IEntitySdj | undefined {
    return (entKeyNum === 0) ? undefined : <IEntitySdj>getFromCoreArray(entKeyNum, this._graph);
  }

  getEntityRefs (entArray: number[] | string[]): IEntitySdj[] {
    const rtnAry: IEntitySdj[] = [];
    each(entArray, (theValue: number | string) => {
      const entRef = <IEntitySdj>getFromCoreArray(theValue, this._graph);
      if (entRef && entRef.sdId !== 0) {
        rtnAry.push(entRef);
      } else {
        this.log(`Unable to find item sdId/sdKey '${theValue}'`, 3);
      }
    });
    return rtnAry;
  }
  calcSdKeyProps(entity: IEntitySdj): SdKeyProps {
    // confirm entity name and sdId, and get from graph instead
    const localEnt = (entity && this._graph[entity.sdId]) ? this._graph[entity.sdId] : undefined;
    if (!localEnt || entity.sdKey !== localEnt.sdKey || !entity.sdProps) {
      this.log(`entity '${entity.sdKey}' does match description '${localEnt?.sdKey}'`);
      return (entity.sdProps) ? cloneDeep(entity.sdProps) : {}; // calcSdKeyProps shouldn't be called on entities that don't have sdProps
    }
    return this._host.lexiconMgr.calcSdKeyProps(entity, this._graph);
  }
  getItem (itemKeyNum: number | string): IItemSdj | undefined {
    return <IItemSdj>getFromCoreArray(itemKeyNum, this._items);
  }
  getItemRefs(itemArray: number[] | string[]): IItemSdj[] {
    const rtnAry: IItemSdj[] = [];
    each(itemArray, (theValue: number | string) => {
      const itemRef = <IItemSdj>getFromCoreArray(theValue, this._items);
      if (itemRef) {
        rtnAry.push(itemRef);
      } else {
        this.log(`Unable to find item sdId/sdKey '${theValue}'`, 3);
      }
    });
    return rtnAry;
  }

  genJI(): DescriptionJI {
    const rtnDescJI: DescriptionJI = {
      sdInfo: genInfoJI(this._sdInfo),
      graph: [],
      items: [],
      lang: this.lang
    };
    let lexEntRefs: EntityJI[] = [],
      lexItemRefs: ItemJI[] = [];

    if (this._lexicons && this._lexicons.length > 0) {
      rtnDescJI.lexicons = clone(this._lexicons);
      each(this._lexicons, (lexId) => {
        const workLex = this._host.lexiconMgr.getByName(lexId);
        lexEntRefs = (workLex?.entities) ? lexEntRefs.concat(workLex.entities) :lexEntRefs;
        lexItemRefs = (workLex?.items) ? lexItemRefs.concat(workLex.items) : lexItemRefs;
      });
    }

    each(this._items, (sdjItem: IItemSdj) => {
      const itemInLex = find(lexItemRefs, {sdId: sdjItem.sdId}),
        itemInBase = find(BASE_ITEMS_JI, {sdId: sdjItem.sdId});
      if (!itemInLex && !itemInBase) {
        rtnDescJI.items.push(sdjItem.genJI());
      }
    });

    each(this._graph, (sdjEnt: IEntitySdj) => {
      const entInLex = find(lexEntRefs, {sdId: sdjEnt.sdId});
      if (sdjEnt.sdId !== 0 && !entInLex) {
        rtnDescJI.graph.push(sdjEnt.genJI());
      }
    });

    if (this._lang !== "en") {
      rtnDescJI.lang = this._lang;
    }

    return rtnDescJI;
  }

  // Use searches for misses; use this function only for Verified Graphs
  // Internals should not be calling for things that don't exist
  getEntityRefById(entId: number): IEntitySdj | undefined {
    return (entId > 0) ? this._graph[entId] : undefined;
  }

  getItemsByEntity(entKeyNum: string | number): IItemSdj[] {
    const actualEnt = <IEntitySdj>getFromCoreArray(entKeyNum, this._graph);
    if (!actualEnt) {
      return []; 
    }
    let extendsList = (actualEnt.extendIds) ? clone(actualEnt.extendIds) : [],
      fullItemList: number[] = cloneDeep(actualEnt.sdItems),
      getEnts = (entRef: IEntitySdj) => {
        let rtnSdItems = cloneDeep(entRef.sdItems);
        if (entRef.extendIds && isArrayWithLen(entRef.extendIds)) {
          each(entRef.extendIds, (num: number) => {
            const subEntRef = <IEntitySdj>getFromCoreArray(num, this._graph);
            if (subEntRef) {
              rtnSdItems = rtnSdItems.concat(getEnts(subEntRef));
            }
          });
        }
        return rtnSdItems;
      };

    each(extendsList, (entNum: number) => {
      const topExtendRef = <IEntitySdj>getFromCoreArray(entNum, this._graph);
      if (topExtendRef) {
        fullItemList = fullItemList.concat(getEnts(topExtendRef));
      }
    });

    fullItemList = uniq(fullItemList.sort());
    return this.getItemRefs(fullItemList);
  }

  verifyJIbyType(ji: SdjJITypes, jiType: ESDJ_CLASS, strict: boolean = false): boolean {
    return this._host.verifyJIbyType(ji, jiType, strict);
  }

  private entityItemBuild(inDescJI: DescriptionJI) {
    this._items = this.buildBaseItems();

    const descriptRef: IDescriptionSdj = <IDescriptionSdj>this;
    each(inDescJI.items, (itemJI: ItemJI) => {
      this.checkBase(itemJI, true);
      this._items.push(new SdjItem(genItemJI(itemJI), descriptRef));
    });

    each(inDescJI.graph, (entityJI: EntityJI) => {
      this.checkBase(entityJI, false);
      let defItemIds = times(BASE_ITEMS_JI.length);
      if (entityJI.sdItems && Array.isArray(entityJI.sdItems)) {
        defItemIds = defItemIds.concat(entityJI.sdItems);
      }
      entityJI.sdItems = defItemIds;
      this._graph.push(new SdjEntity(genEntityJI(entityJI), descriptRef));
    });
    this._graph.unshift(new SdjEntity(genEntityJI(GRAPH_ZERO), this));
    this._lexicons = (inDescJI.lexicons) ? inDescJI.lexicons : [];

    each(this._items, (itemJI: IItemSdj, idx: number) => {
      // Has been pre-sorted, final confirmation to confirm sdId === _item index alignment
      if (idx !== itemJI.sdId) {
        throw new Error("[SDJ] Description validity error, missing entity or error;");
      }
    });

    each(this._graph, (entity: IEntitySdj, idx: number) => {
      // This has been presorted before this, but confirms sdId === _graph index alignment
      if (idx !== entity.sdId) {
        throw new Error("[SDJ] Description validity error, missing entity or error;");
      }
      const classRef: SdjEntity = <SdjEntity>entity;
      classRef.$refreshRefs();
    });
  }
  private buildBaseItems(): SdjItem[] {
    const newBaseItems: ItemJI[] = this._host.lexiconMgr.newBaseItems(),
      rtnArray: SdjItem[] = [];
    each(newBaseItems, (itemJI: ItemJI) => {
      rtnArray.push(new SdjItem(itemJI, this));
    });
    return rtnArray;
  }
  private checkBase(itemBase: CoreSD, isItem: boolean) {
    const checkArray = (isItem) ? this._items : this._graph,
      errorType = (isItem) ? "Item" : "Entity";
    if (SYS_RESERVED.indexOf(itemBase.sdKey) !== -1) {
      throw new Error(`[SDJ] Description: ${errorType} sdKey '${itemBase.sdKey}' is reserved;`);
    }
    if (find(checkArray, {sdKey: itemBase.sdKey})) {
      throw new Error(`[SDJ] Description: ${errorType} sdKey '${itemBase.sdKey}' already exists;`);
    }
    if (find(checkArray, {sdId: itemBase.sdId})) {
      throw new Error(`[SDJ] Description:: ${errorType} sdKey '${itemBase.sdKey}' has duplicate sdId [${itemBase.sdId}];`);
    }
  }

  private strictLogError(logStr: string, strict: boolean) {
    if (strict) {
      throw new Error(logStr);
    } else {
      this.log(logStr, 3);
    }
  }

  static VerifyJI(inDesc: DescriptionJI) {
    if (!isInfo(inDesc.sdInfo)) {
      throw new Error("[SDJ] Description - improperly formatted sdInfo;");
    } else if (isInfo(inDesc.sdInfo) && !validSDKey(inDesc.sdInfo.name)) {
      throw new Error(`[SDJ] Description name '${inDesc.sdInfo.name}' must be sdKey format;`);
    }
    if (inDesc.lexicons && !validLexiconArray(inDesc.lexicons)) {
      throw new Error("[SDJ] Description.lexicons improperly formed;");
    }
    if ((!inDesc.items || !isArray(inDesc.items)) && !inDesc.lexicons) {
      throw new Error("[SDJ] Description - items missing or array error;");
    } else if (!inDesc.lexicons && !verifyUniqKeys(inDesc.items)) {
      throw new Error("[SDJ] Description - items array must contain unique sdKeys and sdIds;");
    }
    if (!inDesc.graph) {
      inDesc.graph = [];
    } else if ((inDesc.graph && !isArray(inDesc.graph)) && !inDesc.lexicons) {
      throw new Error("[SDJ] Description graph is not array;");
    } else if (isArray(inDesc.graph) && !verifyUniqKeys(inDesc.graph)) {
      throw new Error("[SDJ] Description graph does not have unique sdKeys/sdIds;");
    }
    if (inDesc.lang && !getRegEx("lang").test(inDesc.lang)) {
      throw new Error(`[SDJ] lang value can be 2-3 lower case chars; error with value:'${inDesc.lang}';`);
    }

    restrictToAllowedKeys("DescriptionJI:" + inDesc.sdInfo.name,
      ["sdInfo", "items", "graph", "lexicons", "lang"], inDesc);
  }
  static IsEqual(alpha: SdjDescription | DescriptionJI, beta: SdjDescription | DescriptionJI) {
    const betaJI = (beta instanceof SdjDescription) ? beta.genJI() : beta,
      alphaJI = (alpha instanceof SdjDescription) ? alpha.genJI() : alpha;
    return isEqual(alphaJI, betaJI);
  }
}
