unit functions;

//============================================================================
// skip certain files
function FileIndexToSkip(i: integer): Boolean;
begin
  Result := False;
  sCurrentModName := GetFileName(FileByIndex(i));
  // ignoring it because you really mean it
  if SameText(sCurrentModName, 'Skyrim.Hardcoded.keep.this.with.the.exe.and.otherwise.ignore.it.I.really.mean.it.dat') then
    Result := True;
  // ignoring myself
  if SameText(sCurrentModName, 'DynDOLOD.esp') then
    Result := True;
  // still ignoring myself
  if SameText(sCurrentModName, sDynDOLODFileName) then
    Result := True;
  // ignore mods with higher load order than myself
  if i > GetLoadOrder(NEWFile) then
    Result := True;
  // ignoring mods from ini list
  if slModNameIgnore.Count > 0 then
    if wbStringListInString(slModNameIgnore, sCurrentModName) <> -1 then
      Result := True;
  if not Result then
    //MyMessage('  ' + sCurrentModName)
  else
    MyMessage('  Ignoring ' + sCurrentModName);
end;

//============================================================================
// get a list for a filename index
function StringListByFileName(sFileName: String; sl: TStringList): TStringList;
var
  idx: integer;
  sl1, sl2: TStringList;
  s, t: string;
begin
  Result := TStringList.Create;

  sl1 := TStringList.Create;
  sl1.StrictDelimiter := True;
  sl2 := TStringList.Create;
  sl2.StrictDelimiter := True;

  for idx := 0 to Pred(sl.Count) do begin
    s := sl.Names[idx];
    t := sl.Values[s];
    if (Length(s) > 0) and (Length(t) > 0) then begin
      sl1.Clear;
      sl1.CommaText := s;
      sl2.Clear;
      sl2.CommaText := t;
      // first entry always applies to all
      if idx = 0 then begin
        Result.AddStrings(sl2);
      end
      else begin
        if wbStringListInString(sl1, sFileName) <> -1 then
          Result.AddStrings(sl2);
      end;
    end;
  end;
end;

//============================================================================
// load a stringlist
function LoadStringList(sListName: String): TStringList;
begin
  Result := TStringList.Create;
  if not FileExists(AdvancedPath + sListName + '.txt') then
    Exit;
  Result.LoadFromFile(AdvancedPath + sListName + '.txt');
  Result.Add('dummy');
  while Pos('//', Result[0]) = 1 do
    Result.Delete(0);
  Result.Delete(Pred(Result.Count));
end;

//============================================================================
// load a stringlist
function LoadStringListCache(sListName: String): TStringList;
begin
  Result := TStringList.Create;
  if not FileExists(CachePath + sListName + '.txt') then
    Exit;
  Result.LoadFromFile(CachePath + sListName + '.txt');
  Result.Add('dummy');
  while Pos('//', Result[0]) = 1 do
    Result.Delete(0);
  Result.Delete(Pred(Result.Count));
end;

//============================================================================
// LoadOrder FormID to File
function GetFileByFormID(FormID: cardinal): IInterface;
begin
  // because of reasons
  FormID := StrToInt64(IntToStr(FormID));
  FormID := FormID shr 24 and $FF;
  if FormID > LoadOrderCount then begin
    MyMessage('Load order messed up! Tried to find a file id higher than files loaded. ' + IntToStr(FormID) + ' > ' + IntToStr(LoadOrderCount) + '. Aborting script.');
    halt;
  end;
  Result := FileByLoadOrder(FormID);
end;

//============================================================================
// Get Record by Loadorder FormID
function RecordByLoadOrderFormID(FormID: cardinal): IInterface;
begin
  Result := WinningOverride(RecordByFormID(GetFilebyFormID(FormID), FormID, True));
end;

//============================================================================
// Save sl Object with filename and formid in case load order changes
// FormID Index, FormID
procedure SaveOSLToFile(sl: TStringList; sFileName: string);
var
  slExport: TStringList;
  i: integer;
  s: string;
begin
  if sl.Count > 0 then begin
    slExport := TStringList.Create;
    for i := 0 to Pred(sl.Count) do begin
      s := GetFileName(GetFilebyFormID(sl.Objects[i])) + ';' + IntToHex(sl.Objects[i], 8);
      s := s + ';' + GetFileName(GetFilebyFormID(sl[i])) + ';' + IntToHex(sl[i], 8);
      slExport.Add(s);
    end;
    if sl.Count > 1 then
      MyMessage('        Saving ' + sFileName + ' ' + IntToStr(Pred(slExport.Count)));
    slExport.SaveToFile(CachePath + sFileName + '.txt');
    slExport.Free;
  end;
end;

//============================================================================
// Save sl Object with filename and formid in case load order changes
// String Index, FormID
procedure SaveOSLToFile2(sl: TStringList; sFileName: string);
var
  slExport: TStringList;
  i: integer;
  s: string;
begin
  if sl.Count > 0 then begin
    slExport := TStringList.Create;
    for i := 0 to Pred(sl.Count) do begin
      s := GetFileName(GetFilebyFormID(sl.Objects[i])) + ';' + IntToHex(sl.Objects[i], 8);
      slExport.Add(s + ';' + sl[i]);
    end;
    if sl.Count > 1 then
      MyMessage('        Saving ' + sFileName + ' ' + IntToStr(Pred(slExport.Count)));
    slExport.SaveToFile(CachePath + sFileName + '.txt');
    slExport.Free;
  end;
end;

//============================================================================
// Load sl Object
// FormID, FormID
function LoadOSLFromFile(var slImport: TStringList; sFileName: string): boolean;
var
  slLoad, sl: TStringList;
  i, j: integer;
  FormIDIndex, FormIDObject: Cardinal;
begin
  Result := False;

  if not FileExists(CachePath + sFileName + '.txt') then
    Exit;

  slLoad := TStringList.Create;
  slLoad.LoadFromFile(CachePath + sFileName + '.txt');
  if slLoad.Count > 0 then begin
    Result := True;
    sl := TStringList.Create;
    sl.Delimiter := ';';
    sl.StrictDelimiter := True;
    for i := 0 to Pred(slLoad.Count) do begin
      sl.Clear;
      sl.DelimitedText := slLoad[i];
      FormIDIndex := FileNameToLoadOrder(sl[2], StrToInt64('$' + sl[3]));
      FormIDObject := FileNameToLoadOrder(sl[0], StrToInt64('$' + sl[1]));
      if FormIDObject = 1 then
        Continue;
      if Assigned(RecordByLoadOrderFormID(FormIDIndex)) and Assigned(RecordByLoadOrderFormID(FormIDObject)) then
        slImport.AddObject(FormIDIndex, FormIDObject)
      else begin
        MyMessage('        no match for ' + IntToHex(FormIDIndex, 8) + ':' + IntToHex(FormIDObject, 8));
        Result := False;
        Break;
      end;
    end;
    if Result then
      //MyMessage('        Loaded ' + sFileName + ' ' + IntToStr(slImport.Count))
    else
      MyMessage('        Loading cache aborted ');
    sl.Free;
  end;
  slLoad.Free;
end;

//============================================================================
// Load sl Object
// String, FormID
function LoadOSLFromFile2(var slImport: TStringList; sFileName: string; bCache, bIgnoreError: boolean): boolean;
var
  slLoad, sl: TStringList;
  i, j: integer;
  FormIDObject: Cardinal;
  sPath: string;
begin
  Result := False;

  if bCache then
    sPath := CachePath
  else
    sPath := AdvancedPath;

  if not FileExists(sPath + sFileName + '.txt') then
    Exit;

  slLoad := TStringList.Create;
  slLoad.LoadFromFile(sPath + sFileName + '.txt');
  if slLoad.Count > 0 then begin
    Result := True;
    sl := TStringList.Create;
    sl.Delimiter := ';';
    sl.StrictDelimiter := True;
    for i := 0 to Pred(slLoad.Count) do begin
      sl.Clear;
      sl.DelimitedText := slLoad[i];
      FormIDObject := FileNameToLoadOrder(sl[0], StrToInt64('$' + sl[1]));
      if FormIDObject = 1 then
        Continue;
      if Assigned(RecordByLoadOrderFormID(FormIDObject)) then
        slImport.AddObject(sl[2], FormIDObject)
      else begin
        if not bIgnoreError then begin
          MyMessage('        no match for ' + IntToHex(FormIDObject, 8));
          Result := False;
          Break;
        end;
      end;
    end;
    sl.Free;
  end;
  slLoad.Free;
end;

//============================================================================
// Load sl Object
// String, integer
function LoadOSLFromFile3(var slImport: TStringList; sFileName: string): boolean;
var
  slLoad, sl: TStringList;
  i, j: integer;
  iObject: int64;
  sPath: string;
begin
  Result := False;

  sPath := AdvancedPath;

  if not FileExists(sPath + sFileName + '.txt') then
    Exit;

  slLoad := TStringList.Create;
  slLoad.LoadFromFile(sPath + sFileName + '.txt');
  if slLoad.Count > 0 then begin
    Result := True;
    sl := TStringList.Create;
    sl.Delimiter := ';';
    sl.StrictDelimiter := True;
    for i := 0 to Pred(slLoad.Count) do begin
      sl.Clear;
      sl.DelimitedText := slLoad[i];
      iObject := StrToInt64(sl[1]);
      slImport.AddObject(sl[2], iObject)
    end;
    sl.Free;
  end;
  while Pos('//', Result[0]) = 1 do
    slImport.Delete(0);
  slLoad.Free;
end;

//============================================================================
// FileName to LoadOrderIndex
function FileNameToLoadOrder(sFileName: string; FormID: cardinal): cardinal;
var
  i: integer;
begin
  Result := 0;
  FormID := FormID or $FF000000;
  for i := 0 to Pred(FileCount) do begin
    if SameText(GetFileName(FileByIndex(i)), sFileName) then begin
      Result := GetLoadOrderFormID(RecordByLoadOrderFormID(FileFormIDtoLoadOrderFormID(FileByIndex(i), FormID)));
      Exit;
    end;
  end;
  //MyMessage(' No match! ' + sFileName + ' = ' + IntToHex(FormID, 8));
end;

//============================================================================
// create an override copy unless it already exists in this File
function CopyOverrideToFile(Element, File: IInterface): IInterface;
var
  i: integer;
  s: string;
  sl: TStringList;
begin
  sl := TStringList.Create;
  sl.Duplicates := dupIgnore;
  sl.Sorted := True;
  Element := WinningOverride(Element);
  Result := Element;
  if GetFile(Element) <> File then begin
    AddMasterIfMissing(File, GetMaster(MasterOrSelf(Element)));
    AddMasterIfMissing(File, GetMaster(WinningOverride(Element)));
    ReportRequiredMasters(Element, sl, True, True);
    for i := 0 to Pred(sl.Count) do
      AddMasterIfMissing(File, CheckMaster(sl[i], Element));
    for i := 0 to Pred(ElementCount(Element)) do
      if Assigned(LinksTo(ElementByIndex(Element, i))) then begin
        s := GetMaster(MasterOrSelf(LinksTo(ElementByIndex(Element, i))));
        if s <> sDynDOLODFileName then
          AddMasterIfMissing(File, s);
        s := GetMaster(WinningOverride(LinksTo(ElementByIndex(Element, i))));
        if s <> sDynDOLODFileName then
          AddMasterIfMissing(File, s);
      end;
    // make sure all required masters are added
    if ElementExists(Element, 'Worldspace') then
      CopyOverrideToFile(LinksTo(ElementByPath(Element, 'Worldspace')), File);
    // make sure all required masters are added
    if ElementExists(Element, 'Cell') then
      CopyOverrideToFile(LinksTo(ElementByPath(Element, 'Cell')), File);
    Result := WinningOverride(wbCopyElementToFile(Element, File, False, True));
  end;
  sl.Free;
end;

//============================================================================
// copy data from source Element to destination Element
procedure CopyREF(Source, Destination: IInterface);
begin
  if ElementExists(Source, 'XSCL') then
    SetElementNativeValues(Destination, 'XSCL', GetElementNativeValues(Source, 'XSCL'));
  if ElementExists(Source, 'DATA') then
    ElementAssign(ElementByPath(Destination, 'DATA'), LowInteger, ElementByPath(Source, 'DATA'), False);
end;

//============================================================================
// set up Firstborn List by Cell x, y
procedure AddToFirstbornList(x, y: integer; bLimit: boolean);
begin
  if not bLimit or ((x > SkyrimWorld.XMin) and (x < SkyrimWorld.XMax) and (y > SkyrimWorld.YMin) and (y < SkyrimWorld.YMax)) then begin
    slCellsWithObjects.Add(CellsToID(x, y));
    if x + 128 < SkyrimWorldUsed.XMin then
      SkyrimWorldUsed.XMin := x + 128;
    if x + 128 > SkyrimWorldUsed.XMax then
      SkyrimWorldUsed.XMax := x + 128;
    if y + 128 < SkyrimWorldUsed.YMin then
      SkyrimWorldUsed.YMin := y + 128;
    if y + 128 > SkyrimWorldUsed.YMax then
      SkyrimWorldUsed.YMax := y + 128;
  end;
end;

//============================================================================
// set up Firstborn List by Element
procedure AddToFirstbornListElement(Element: IInterface; bLimit: boolean);
var
  x, y: float;
begin
  x := GetElementNativeValues(Element, 'DATA\Position\X');
  y := GetElementNativeValues(Element, 'DATA\Position\Y');
  AddToFirstbornListxy(x, y, bLimit); 
end;

//============================================================================
// set up Firstborn List by pos x, y
procedure AddToFirstbornListxy(fx, fy: float; bLimit: boolean);
var
  x, y: integer;
begin
  x := PosToCell(fx);
  y := PosToCell(fy);
  AddToFirstbornList(x, y, bLimit);
end;

//============================================================================
// retrun a mesh path for a LOD level
function LODMeshFromList(Element: IInterface; sLODList: string; LODType: integer): string;
var
  sEDID, sLOD: string;
  iLODLevel: integer;
begin
  Result := '';
  sEDID := EditorID(Element);
  slLODs.DelimitedText := sLODList;

  if LodType = iLODNone then
    Exit
  else if LodType = iLODFull then begin
    Result := NormalizePath(Lowercase(GetElementEditValues(Element, 'Model\MODL')));
    Exit;
  end
  else if LODType = iLOD4 then
   iLODLevel := 0
  else if LODType = iLOD8 then
   iLODLevel := 1
  else if LODType = iLOD16 then
    iLODLevel := 2
  else
    Exit;

  sLOD := Lowercase(HexArrayToStr(GetElementEditValues(Element, 'MNAM\LOD #' + IntToStr(iLODLevel) + ' (Level ' + IntToStr(iLODLevel) + ')\Mesh')));

  // do not change already existing *h.nif
  if Pos('h.nif', sLOD) > 0 then
    slLODs[iLODLevel] := sLOD;
  // in case of HeavySnow in the EDID use *h.nif version instead of *l.nif version
  if (Pos('l.nif', slLODs[iLODLevel]) > 0) and (Pos('_heavysn', Lowercase(sEDID)) > 0) then
    slLODs[iLODLevel] := Copy(slLODs[iLODLevel], 0, Pos('l.nif', slLODs[iLODLevel]) - 1) + 'h.nif';
  Result := slLODs[iLODLevel];

  if Result <> '' then
    Result := NormalizePath(Result);
end;

//============================================================================
// add a rule to RuleCache if it matches mesh rule and return idx
function AddRuleToCache(FormID: cardinal; SlashRule: boolean): integer;
var
  i, j: integer;
  Element: IInterface;
  s: string;
  b: boolean;
begin
  Element := RecordByLoadOrderFormID(FormID);
  Result := slRulesCache.IndexOfObject(FormID);
  if Result = -1 then begin
    s := Lowercase(LODMeshFor(Element, iLODFull));
    for i := 0 to Pred(slLOD.Count) do begin
      // add rules for each matching Model
      if Pos(slLOD[i], s) > 0 then begin
        if SlashRule or ((slLOD[i] <> '\') and not SlashRule) then begin
          j := slLOD.Objects[i];
          Result := slRulesCache.Count;
          slRulesCache.AddObject(j, FormID);
          break;
        end;
      end;
    end;
  end;
end;

//============================================================================
// get the rule for a base element
function GetRule(Element: IInterface): integer;
var
  i, idx: integer;
  FormID: cardinal;
  s: string;
begin
  Result := 0;
  FormID := GetLoadOrderFormID(Element);

  // get from cache if possible
  idx := slRulesCache.IndexOfObject(FormID);
  if idx <> -1 then begin
    Result := slRulesCache[idx];
    Exit;
  end;

  s := Lowercase(LODMeshFor(Element, iLODFull));
  for i := 0 to Pred(slLOD.Count) do begin
    if Pos(slLOD[i], s) > 0 then begin
      Result := slLOD.Objects[i];
      break;
    end;
  end;
end;

//============================================================================
// add a DynDOLOD_LOD base element
function AddDynDOLODBaseElement(Element: IInterface; rules: integer): Cardinal;
var
  TempElement: IInterface;
  s, sEDID, sMODLPath, sMODL, sLODModel, sLODList: string;
  i, idx, grid, ref: integer;
begin
  Result := 0;

  idx := olBaseElementDynDOLOD.IndexOfObject(GetLoadOrderFormID(Element));

  // does a DynDOLOD base element exist already? return new formid
  if idx <> -1 then begin
    if IntToHex(olBaseElementDynDOLOD[idx], 8) <> IntToHex(olBaseElementDynDOLOD.Objects[idx], 8) then begin
      Result := StrToInt64(olBaseElementDynDOLOD[idx]);
      Exit;
    end;
  end;

  if idx = -1 then begin
    idx := olBaseElementDynDOLOD.Count;
    olBaseElementDynDOLOD.AddObject(GetLoadOrderFormID(Element), GetLoadOrderFormID(Element));
  end;

  sEDID := EditorID(Element);
  // DynDOLOD_LOD already exists
  if olEDIDExists.IndexOf(sEDID + '_DynDOLOD_LOD') <> -1 then begin
    olBaseElementDynDOLOD[idx] := GetLoadOrderFormID(Element);
    Result := olEDIDExists.Objects[olEDIDExists.IndexOf(sEDID + '_DynDOLOD_LOD')];
    Exit;
  end;

  grid := rules shr 12 and $F;
  ref  := rules shr 16 and $F;

  sMODLPath := Lowercase(GetElementNativeValues(Element, 'Model\MODL'));
  // base the LOD filename on the high poly file name
  sMODL := ExtractFileName(Copy(sMODLPath, 0, Pos('.nif', sMODLPath)-1));

  // find texture variants
  s := LODTexture(Element);
  sLODList := slLODFiles.Values[sMODL + s];
  if sLODList = '' then
    sLODList := slLODFiles.Values[sMODL];

  // water needs to stay ACTI with WNAM else may cause CTD, use full model or striped down dyndolod_lod
  // everything else add as MSTT so it can be more easily resetted, STAT in case there is material shader
  if ElementExists(Element, 'WNAM') then
    TempElement := Add(GroupBySignature(NEWFile, Signature(Element)), Signature(Element), True)
  else
    if ElementExists(Element, 'DNAM') and Assigned(LinksTo(ElementByPath(Element, 'DNAM\Material'))) then
      TempElement := Add(GroupBySignature(NEWFile, 'STAT'), 'STAT', True)
    else
      TempElement := Add(GroupBySignature(NEWFile, 'MSTT'), 'MSTT', True);

  // give this a cool name
  SetElementNativeValues(TempElement, 'EDID', sEDID + '_DynDOLOD_LOD');

  Add(TempElement, 'Model', True);

  // dyndolod_lod model trumps everything else
  if slDynDOLODFiles.Values[sMODL] <> '' then begin
    sLODModel := slDynDOLODFiles.Values[sMODL];
    // set "high detail LOD" for anything that can not be a static LOD
    SetElementNativeValues(TempElement, 'Record Header\Record Flags', $20000);
  end
  else if (grid and 1 = 1) and (ref <> iReferenceReplace) and (sLODList <> '') then begin
    // odd rules are LOD model, but skip to use full model if referencereplace is selected for continues animation
    slLODs.DelimitedText := sLODList;
    sLODModel := slLODs[0];
  end
  else begin
    // even rules are full model
    sLODModel := LODMeshFor(Element, iLODFull);
    // set "high detail LOD" for anything that can not be a static LOD
    SetElementNativeValues(TempElement, 'Record Header\Record Flags', $20000);
  end;

  SetElementNativeValues(TempElement, 'Model\MODL', sLODModel);

  // if "high detail LOD" is set
  if GetElementNativeValues(TempElement, 'Record Header\Record Flags') and $20000 = $20000 then begin
    // copy alternate textures
    if ElementExists(Element, 'Model\MODS') then begin
      for i := 0 to Pred(ElementCount(ElementByPath(Element, 'Model\MODS'))) do
        if Assigned(MasterOrSelf(LinksTo(ElementByPath(ElementByIndex(ElementByPath(Element, 'Model\MODS'), i), 'New Texture')))) then
          AddMasterIfMissing(NEWFile, GetMaster(MasterOrSelf(LinksTo(ElementByPath(ElementByIndex(ElementByPath(Element, 'Model\MODS'), i), 'New Texture')))));
      Add(ElementByPath(TempElement, 'Model'), 'MODS', True);
      ElementAssign(ElementByPath(TempElement, 'Model\MODS'), LowInteger, ElementByPath(Element, 'Model\MODS'), False);
    end;
    // copy water type
    if ElementExists(Element, 'WNAM') then begin
      if Assigned(MasterOrSelf(LinksTo(ElementByPath(Element, 'WNAM')))) then
        AddMasterIfMissing(NEWFile, GetMaster(MasterOrSelf(LinksTo(ElementByPath(Element, 'WNAM')))));
      // set native without the intohex does not work, results in wrong master, doh!?
      SetElementEditValues(TempElement, 'WNAM', IntToHex(GetLoadOrderFormID(LinksTo(ElementByPath(Element, 'WNAM'))),8));
    end;
  end;

  // snow/ash material to LOD versions, even for full models or else it wont work in-game
  if ElementExists(Element, 'DNAM') then begin
    if Assigned(MasterOrSelf(LinksTo(ElementByPath(Element, 'DNAM\Material')))) then
      AddMasterIfMissing(NEWFile, GetMaster(MasterOrSelf(LinksTo(ElementByPath(Element, 'DNAM\Material')))));
    SetElementNativeValues(TempElement, 'DNAM\Max Angle (30-120)', GetElementNativeValues(Element, 'DNAM\Max Angle (30-120)'));
    if Pos('snow', Lowercase(EditorID(LinksTo(ElementByPath(Element, 'DNAM\Material'))))) > 0 then
      SetElementNativeValues(TempElement, 'DNAM\Material', FileNameToLoadOrder('Skyrim.esm', $06AE38));
    if Pos('_ash', Lowercase(EditorID(LinksTo(ElementByPath(Element, 'DNAM\Material'))))) > 0 then
      SetElementNativeValues(TempElement, 'DNAM\Material', FileNameToLoadOrder('Dragonborn.esm', $032720));
  end;

  olBaseElementDynDOLOD[idx] := GetLoadOrderFormID(TempElement);
  olMODLFileBaseElement.AddObject(sMODLPath, GetLoadOrderFormID(TempElement));

  Result := GetLoadOrderFormID(TempElement);
end;

//============================================================================
// add a DynDOLOD_LOD REFR element
function AddDynDOLODREFRElement(Element, BaseElement, BaseElementOrg: IInterface; sType: string): IInterface;
var
  TempElement: IInterface;
  idx: integer;
  s: string;
begin

  s := RemoveNonAlphaNum(GetFileName(GetFile(MasterOrSelf(Element))) + '_' + IntToHex(GetLoadOrderFormID(Element) and $00FFFFFF, 6) + '_DynDOLOD_'+sType);

  // skip if done before
  if olEDIDExists.IndexOf(s) <> -1 then begin
    Result := RecordByLoadOrderFormID(olEDIDExists.Objects[olEDIDExists.IndexOf(s)]);
    // when updating get real override record, not ourself
    if OverrideCount(Result) >= 0 then begin
      if SameText(GetFileName(GetFile(Result)), sDynDOLODFileName) then begin
        if OverrideCount(Result) = 0 then
          Result := MasterOrSelf(Result)
        else
          Result := OverrideByIndex(Result, OverrideCount(Result) - 1);
      end;
    end;
    // add to object data json
    AddDynDOLODREFERValues(Element, Result, BaseElement, BaseElementOrg);
    // for json list
    olSignatureFormID.AddObject(Signature(BaseRecord(Result)), GetLoadOrderFormID(Result));
    Exit;
  end;

  // child world?
  idx := olREFRWorld.IndexOfObject(GetLoadOrderFormID(Element));
  if idx <> -1 then
    sThisWorld := olREFRWorld[idx]
  else
    sThisWorld := sWorld;

  TempElement := Add(CopyOverrideToFile(GetCellFromWorldspace(LODGenWorld, 0, 0, True), NEWFILE), 'REFR', True);

  // pos, rot, scale
  CopyREF(Element, TempElement);

  // persistant, initially disabled, neverfade, norespawn
  SetElementNativeValues(TempElement, 'Record Header\Record Flags', $40010C00);
  SetElementNativeValues(TempElement, 'EDID', s);
  SetElementNativeValues(TempElement, 'NAME', GetLoadOrderFormID(BaseElement));

  AddDynDOLODREFERValues(Element, TempElement, BaseElement, BaseElementOrg);
  Result := TempElement;

  // for json list
  olSignatureFormID.AddObject(Signature(BaseRecord(Result)), GetLoadOrderFormID(Result));
end;

//============================================================================
// add VMAD, Active Parents, Enable Parent to a REFR
procedure AddDynDOLODREFERValues(ElementOrg, Element, BaseElement, BaseElementOrg: IInterface);
var
  TempElement, EnableParent: IInterface;
  s, sEDID, sMODLPath: string;
  i, idx, rules: integer;
  isOpposite, isEnabled, isFarGrid, isSwitch, isKeep, isDistant, isWorld: Boolean;
begin
  EnableParent := ElementOrg;

  // get rule for reference
  idx := slRulesCache.IndexOfObject(GetLoadOrderFormID(ElementOrg));
  if idx <> -1 then
    rules := slRulesCache[idx]
  else begin
    // get rule for original base element
    idx := slRulesCache.IndexOfObject(GetLoadOrderFormID(BaseElementOrg));
    if idx <> -1 then
      rules := slRulesCache[idx]
    else begin
      // this is a town copy already linking to a DynDOLOD_LOD, need to get rule to original base element
      idx := olBaseElementDynDOLOD.IndexOf(GetLoadOrderFormID(BaseElementOrg));
      if idx <> -1 then begin
        idx := slRulesCache.IndexOfObject(olBaseElementDynDOLOD.Objects[idx]);
        if idx <> -1 then
          rules := slRulesCache[idx]
        else 
          rules := 0;
      end
      else 
        rules := 0;
    end;
  end;

  if (rules shr 12 and $F = iGridFarLOD) or (rules shr 12 and $F = iGridFarFull) then
    isFarGrid := True
  else
    isFarGrid := False;

  if (rules shr 12 and $F = iGridNeverFadeLOD) or (rules shr 12 and $F = iGridNeverFadeFull) then
    isDistant := True
  else
    isDistant := False;

  if rules shr 16 and $F = iReferenceReplace then
    isSwitch := True
  else
    isSwitch := False;

  if rules shr 16 and $F = iReferenceKeep then
    isKeep := True
  else
    isKeep := False;

  // FormID is not in the world - child town
  if sWorld <> sThisWorld then
    isWorld := True
  else
    isWorld := False;

  isEnabled := True;
  isOpposite := False;

  sEDID := EditorID(BaseElement);

  // initially disabled?
  if GetIsInitiallyDisabled(ElementOrg) then
    isEnabled := False;

  // check the XESP chain to set the initial enable status
  // and find highest enable parent
  TempElement := ElementOrg;

  while ElementExists(TempElement, 'XESP') do begin
    // is the enable parent initially disabled?
    if GetIsInitiallyDisabled(LinksTo(ElementByPath(TempElement, 'XESP\Reference'))) then
      isEnabled := False
    else
      isEnabled := True;
    // is the opposite parent flag set?
    if Assigned(ElementByPath(TempElement, 'XESP\Flags')) then begin
      if (GetElementNativeValues(TempElement, 'XESP\Flags') and 1) = 1 then begin
        isEnabled := not isEnabled;
        isOpposite := not isOpposite;
      end;
    end;
    // check the next parent
    TempElement := LinksTo(ElementByPath(TempElement, 'XESP\Reference'));
    // Only references have XESP, nothing to follow up
    if Signature(TempElement) <> 'REFR' then
      break;
  end;
  // use XESP enableparent for VMAD enableparent
  EnableParent := TempElement;

  // VMAD
  wbCopyElementToRecord(ElementByPath(RecordByLoadOrderFormID(olEDIDFormID.Objects[olEDIDFormID.IndexOf('LODObject')]), 'VMAD'), Element, True, True);

  //Activate Parents
  wbCopyElementToRecord(ElementByPath(RecordByLoadOrderFormID(olEDIDFormID.Objects[olEDIDFormID.IndexOf('LODObject')]), 'Activate Parents'), Element, True, True);

  // add to Near/FarGrid
  if isFarGrid or isDistant then begin
    //MyMessage('Firstborn ' + Name(BaseElement));
    idx := olFirstborns.IndexOf(PosToID(GetElementNativeValues(Element,'DATA\Position\X'), GetElementNativeValues(Element,'DATA\Position\Y')));
    if idx = -1 then
      MyMessage('  no Firstborn for ' + CellsToString(PosToCell(GetElementNativeValues(Element,'DATA\Position\X')), PosToCell(GetElementNativeValues(Element,'DATA\Position\Y'))) + ' ' + Name(Element));
    SetElementNativeValues(Element, 'Activate Parents\Activate Parent Refs\XAPR - Activate Parent Ref\Reference', olFirstborns.Objects[idx]);
  end
  else begin
    //MyMessage('Minion ' + Name(BaseElement));
    idx := olMinions.IndexOf(PosToID(GetElementNativeValues(Element,'DATA\Position\X'), GetElementNativeValues(Element,'DATA\Position\Y')));
    if idx = -1 then
      MyMessage('  no Minion for ' + CellsToString(PosToCell(GetElementNativeValues(Element,'DATA\Position\X')), PosToCell(GetElementNativeValues(Element,'DATA\Position\Y'))) + ' ' + Name(Element));
    SetElementNativeValues(Element, 'Activate Parents\Activate Parent Refs\XAPR - Activate Parent Ref\Reference', olMinions.Objects[idx]);
  end;

  //isDistantFade - never turn off if distant
  if isDistant then begin
    s := '""'; 
    if isEnabled then
      SetIsInitiallyDisabled(Element, False);
  end
  else
    s := '"1"';

  // isFadeOut - wait before turning off LOD object when cell attaches
  if Pos('\trees\', Lowercase(GetElementNativeValues(BaseElement, 'Model\MODL'))) > 0 then
    s := s + ',""'
  else
    s := s + ',"1"';

  // isInitiallyEnabled
  if not isEnabled and not isSwitch then
    s := s + ',""'
  else
    s := s + ',"1"';

  // isKeep - do not turn off when cell is attached, do not change original model
  if isKeep then begin
    s := s + ',""';
  end
  // isSwitch - do not turn off when cell is attached
  // do this so animation is continues
  // override and disable original element
  // if there is no XESP enable parent, set enable parent to self
  else if isSwitch or (GetLoadOrderFormID(Element) = GetLoadOrderFormID(ElementOrg)) then begin
    // isSwitch
    s := s + ',""';
    // if there is a original REFR it needs to be disabled
    if GetLoadOrderFormID(Element) <> GetLoadOrderFormID(ElementOrg) then begin
      ElementOrg := CopyOverrideToFile(ElementOrg, NEWFILE);
      // set enable parent of DynDOLOD to itself
      EnableParent := Element;
      SetIsInitiallyDisabled(ElementOrg, True);
      if not ElementExists(ElementOrg, 'XESP') then
        Add(ElementOrg, 'XESP', True);
      // disable and set opposite player and below ground
      SetElementNativeValues(ElementOrg, 'XESP\Reference', 20);
      SetElementNativeValues(ElementOrg, 'XESP\Flags', 1);
      if not GetIsPersistent(ElementOrg) then
        SetElementNativeValues(ElementOrg, 'DATA\Position\Z', -30000);
    end;
  end
  else
    s := s + ',"1"';

  // if the original element is going to be permanently disabled and has no XESP, set enable parent of DynDOLOD to self
  if (slREFRToDisable.IndexOf(GetLoadOrderFormID(ElementOrg)) <> -1) and not ElementExists(ElementOrg, 'XESP') then begin
    EnableParent := Element;
    isOpposite := False;
  end;

  // isOppositeParent XESP flag
  if isOpposite then
    s := s + ',"1"'
  else
    s := s + ',""';

  // MyParentFormID - not using linked reference or object reference because "enable parent" would need to be persistant
  s := s + ',"' + IntToStr(GetLoadOrderFormID(EnableParent) and $00FFFFFF) + '"';
  // MyParentString
  s := s + ',"' + GetFileName(GetFilebyFormID(GetLoadOrderFormID(EnableParent))) + '"';

  // Destructable base element
  if ElementExists(BaseElementOrg, 'Destructable') then begin

    TempElement := ElementByPath(BaseElementOrg, 'Destructable\Stages');

    // assuming that the Model\DMDL with highest stage number is the final model, only then the LOD model changes
    for i := Pred(ElementCount(TempElement)) downto 0 do begin
      if ElementExists(ElementByIndex(TempElement, i), 'Model') then begin

        // MyStageIndex
        s := s + ',"' + IntToStr(GetElementNativeValues(ElementByIndex(TempElement, i), 'DSTD\Model Damage Stage')) + '"';

        // MyStageType
        // -1 nothing to do ever, default
        //  0 off if destruction stage >= MyStageIndex (for initial default model)
        //  1 off if destruction stage <  MyStageIndex (for final destruction model)

        // check if a DynDOLOD base element exists for the destruction model and add a REFR
        sMODLPath := Lowercase(GetElementNativeValues(ElementByIndex(TempElement, i), 'Model\DMDL'));
        idx := olMODLFileBaseElement.IndexOf(sMODLPath);
        if idx <> -1 then begin
          if RecordByLoadOrderFormID(olMODLFileBaseElement.Objects[idx]) <> BaseElement then begin
            s := s + ',""';
            // Add a REFR for destruction model
            AddDynDOLODREFRElement(ElementOrg, RecordByLoadOrderFormID(olMODLFileBaseElement.Objects[idx]), BaseElementOrg, 'STAGE');
          end
          else begin
            // if the base elements are the same this is the destruction model, change MyStageType
            s := s + ',"1"';
          end;
        end
        else
          s := s + ',""';

        break;

      end;
      s := s + ',"-1","-1"';
    end;

  end
  else
    s := s + ',"-1","-1"';

  olJsonObjectData.AddObject(s, GetLoadOrderFormID(Element));

end;
      

//============================================================================
// override and disable a neverfade
function DisableNeverfade(Element: IInterface; s: string): IInternal;
begin
  Result := CopyOverrideToFile(Element, NEWFILE);
  SetElementNativeValues(Result, 'Record Header\Record Flags', GetElementNativeValues(Result, 'Record Header\Record Flags') and not $10000);
  MyMessage('        disabled NeverFade, replaced by ' + s + ' LOD for ' + Name(Element));
end;

//============================================================================
// set VMAD scripts values
procedure SetScriptValue(Element: IInterface; sScriptName, sPropertyName, sPropertyType, sValue: string);
var
  TempElement: IInterface;
  i: integer;
begin
  TempElement := ElementByPath(Element, 'VMAD - Virtual Machine Adapter\Data\Scripts');
  for i := 0 to Pred(ElementCount(TempElement)) do begin
    if GetEditValue(ElementByName(ElementByIndex(TempElement, i), 'scriptName')) = sScriptName then begin
      TempElement := ElementByIndex(TempElement, i);
      TempElement := ElementByName(TempElement, 'Properties');
      for i := 0 to Pred(ElementCount(TempElement)) do begin
        if GetEditValue(ElementByName(ElementByIndex(TempElement, i), 'propertyName')) = sPropertyName then begin
          TempElement := ElementByPath(ElementByIndex(TempElement, i), sPropertyType);
          SetNativeValue(TempElement, sValue);
          //SetEditValue(TempElement, sValue);
          Exit;
        end;
      end;
    end;
  end;
end;

//============================================================================
// set VMAD scripts values
function GetScriptValue(Element: IInterface; sScriptName, sPropertyName, sPropertyType: string): string;
var
  TempElement: IInterface;
  i: integer;
begin
  Result := '';
  TempElement := ElementByPath(Element, 'VMAD - Virtual Machine Adapter\Data\Scripts');
  for i := 0 to Pred(ElementCount(TempElement)) do begin
    if GetEditValue(ElementByName(ElementByIndex(TempElement, i), 'scriptName')) = sScriptName then begin
      TempElement := ElementByIndex(TempElement, i);
      TempElement := ElementByName(TempElement, 'Properties');
      for i := 0 to Pred(ElementCount(TempElement)) do begin
        if GetEditValue(ElementByName(ElementByIndex(TempElement, i), 'propertyName')) = sPropertyName then begin
          TempElement := ElementByPath(ElementByIndex(TempElement, i), sPropertyType);
          Result := GetEditValue(TempElement);
          Exit;
        end;
      end;
    end;
  end;
end;

//============================================================================
// x/y into a single value
function PosToID(x, y : float): Cardinal;
begin
  Result := CellsToID(PosToCell(x), PosToCell(y));
end;

//============================================================================
// x/y into a single value
function CellsToID(x, y : int64): Cardinal;
begin
  Result := ((x + 128) * 256) + (y + 128);
end;

//============================================================================
function EDIDToID(Which, EDID : string) : string;
var
  regexp : PerlRegEx;
begin
  Result := '';
  EDID := StringReplace(EDID, '%', '-', [rfReplaceAll]);
  regexp := TPerlRegEx.Create;
  regexp.RegEx := '^' + Which + '_+((-?\d+)_+(-?\d+))$';
  regexp.Options := [];
  regexp.Subject := EDID;
  if regexp.MatchAgain then begin
    Result := CellsToID(regexp.Groups[2], regexp.Groups[3]);
  end;
  regexp.Free;
end;

//============================================================================
function PosToCell(Position : float): int64;
begin
  if (Position < 0) then
    Result := Position div CellSize - 1
  else Result := Position div CellSize;
end;

//============================================================================
function CellToPos(Cell : int64): float;
begin
  Result := (Cell * CellSize) + (CellSize / 2);
end;

//============================================================================
function IDToCells(Which : string; ID : int64) : int64;
begin
  if Which='x' then Result := ((ID and 65280) / 256) - 128 else Result := (ID and 255) - 128;
end;

//============================================================================
function CellsToString(x, y : int64): string;
begin
  Result := IntToStr(x) + '_' + IntToStr(y);
end;

//============================================================================
function StrToHexArray(s: string): string;
var
  i: integer;
  c: char;
begin
  Result := '';
  for i := 1 to 260 do begin
    if i <= Length(s) then
      c := Copy(s, i, 1)
    else c := #0;
    Result := Result + IntToHex(ord(c), 2) + ' '
  end;
  Result := Trim(Result);
end;

//============================================================================
// remove non alphanumeric
// EDID only allows a-z 1-9 and _%&
function RemoveNonAlphaNum(Text: string): string;
var
  regexp : PerlRegEx;
begin
  Result := '';
  regexp := TPerlRegEx.Create;
  regexp.RegEx := '[\w_%&]*';
  regexp.Options := [];
  regexp.Subject := Text;
  while regexp.MatchAgain do begin
    Result := Result + regexp.Groups[0];
  end;
  regexp.Free;
end;

//============================================================================
// guess static LOD filenames based on texture replacement
function LODTexture(Element: IInterface): string;
var
  MODS, TempElement: IInterface;
  index, idx, idx3D: integer;
  sEDID, sMODLPath, sMODL: string;
  FormID: cardinal;
begin
  Result := '';

  MODS := ElementByPath(Element, 'Model\MODS');
  if not Assigned(MODS) then
    Exit;

  // get high poly path\to\filename
  sMODLPath := Lowercase(GetElementNativeValues(Element, 'Model\MODL'));
  // get the high poly filename
  sMODL := ExtractFileName(Copy(sMODLPath, 0, Pos('.nif', sMODLPath) - 1));

  // if there is an index in the texture_set file use it
  idx3D := slMODLTextureSet.IndexOf(sMODL);

  // lookup texture name by 3D index
  if idx3D <> -1 then begin
    // nifty resuse of idx3D
    idx3D := slMODLTextureSet.Objects[idx3D]);
    for index := 0 to Pred(ElementCount(MODS)) do begin
      TempElement := ElementByIndex(MODS, index);
      if idx3D = GetElementNativeValues(TempElement, '3D Index') then begin
        idx := slTexturesNames.IndexOfObject(FormID);
        if idx <> -1 then begin
          Result := slTexturesNames[idx];
          Exit;
        end;
      end;
     end
  end;

  // crudley looking for a match, first come first serve
  for index := 0 to Pred(ElementCount(MODS)) do begin
    TempElement := ElementByIndex(MODS, index);
    // skip everything with decal in its name
    if Pos('decal', Lowercase(GetElementNativeValues(TempElement, '3D Name'))) = 0 then begin
      FormID := GetLoadOrderFormID(LinksTo(ElementByPath(TempElement, 'New Texture')));
      idx := slTexturesNames.IndexOfObject(FormID);
      if idx <> -1 then begin
        Result := slTexturesNames[idx];
        Exit;
      end;
    end;
  end;

end;

//============================================================================
// LOD material detection
function LODMaterial(Element: IInterface): string;
var
  s: string;
begin
  Result := '';
  s := EditorID(LinksTo(ElementByPath(Element, 'DNAM\Material')));
  if Pos('Snow', s) > 0 then
    Result := 'Snow';
  if Pos('Ash', s) > 0 then
    Result  := 'Ash';
end;

//============================================================================
// found in other scripts and copied without shame
function GetCellFromWorldspace(Worldspace: IInterface; GridX, GridY: integer; bPersistant: boolean): IInterface;
var
  blockidx, subblockidx, cellidx: integer;
  wrldgrup, block, subblock, cell: IInterface;
  Grid, GridBlock, GridSubBlock: TwbGridCell;
  LabelBlock, LabelSubBlock: Cardinal;
begin

  Grid := wbGridCell(GridX, GridY);

  if bPersistant then begin
    cellidx := olEDIDFormID.IndexOf(sThisWorld + 'PersistantCELL');
    if cellidx <> -1 then begin
      Result := RecordByLoadOrderFormID(olEDIDFormID.Objects[cellidx]);
      Exit;
    end;
  end
  else begin
    cellidx := olXCLCCache.IndexOf(IntToStr(Grid.x) + ',' + IntToStr(Grid.y));
    if cellidx <> -1 then begin
      Result := RecordByLoadOrderFormID(olXCLCCache.Objects[cellidx]);
      Exit;
    end;
  end;

  GridSubBlock := wbSubBlockFromGridCell(Grid);
  LabelSubBlock := wbGridCellToGroupLabel(GridSubBlock);
  GridBlock := wbBlockFromSubBlock(GridSubBlock);
  LabelBlock := wbGridCellToGroupLabel(GridBlock);

  wrldgrup := ChildGroup(Worldspace);
  // iterate over Exterior Blocks
  for blockidx := 0 to Pred(ElementCount(wrldgrup)) do begin
    block := ElementByIndex(wrldgrup, blockidx);
    if bPersistant then begin 
      if GetIsPersistent(block) then begin
        olEDIDFormID.AddObject(sThisWorld + 'PersistantCELL', GetLoadOrderFormID(block));
        Result := block;
        Exit;
      end;
    end
    else begin
      if GroupLabel(block) <> LabelBlock then Continue;
      // iterate over SubBlocks
      for subblockidx := 0 to Pred(ElementCount(block)) do begin
        subblock := ElementByIndex(block, subblockidx);
        if GroupLabel(subblock) <> LabelSubBlock then Continue;
        // iterate over Cells
        for cellidx := 0 to Pred(ElementCount(subblock)) do begin
          cell := ElementByIndex(subblock, cellidx);
          if (Signature(cell) <> 'CELL') then Continue;
          if (GetElementNativeValues(cell, 'XCLC\X') = Grid.x) and (GetElementNativeValues(cell, 'XCLC\Y') = Grid.y) then begin
            olXCLCCache.AddObject(IntToStr(Grid.x) + ',' + IntToStr(Grid.y), GetLoadOrderFormID(cell));
            Result := cell;
            Exit;
          end;
        end;
      end;
      Break;
    end;
    Break;
  end;
end;

//============================================================================
// timestamp are a hoot
procedure MyMessage(s: string);
begin
  AddMessage('[' + FormatDateTime('hh:nn:ss.zzz', Now - Time) + ']'#9 + s);
end;

//============================================================================
// check the master file name because adding correct masters is an art
function CheckMaster(s: string; Element: IInterface): string;
begin
  // in the rare case one of the 6 built-in elements is linked to
  if SameText(s, 'Skyrim.Hardcoded.keep.this.with.the.exe.and.otherwise.ignore.it.I.really.mean.it.dat') then
    s := 'Skyrim.esm';
  Result := s;
  if (Pos('.esp', s) = 0) and (Pos('.esm', s) = 0) then begin
    MyMessage('Non esm/esp master ' + s + ' for ' + Name(Element));
    Result := '';
  end;
end;

//============================================================================
// get the master file name
function GetMaster(Element: IInterface): string;
begin
  Result := CheckMaster(GetFileName(GetFile(Element)), Element);
end;

end.

