unit process;

// update STAT with LOD, check DOOR/ACTI/MSTT/TREE for LOD candiates
// create STAT for DynDOLOD
// scan worlds and export LODGen list, create REFR for DynDOLOD Mod from scan and manual list

//============================================================================
// Iterate through STAT/DOOR/ACTI/MSTT/TREE to find suitable base elements to update
procedure DoBaseElements;
var
  group, Element, TempElement: IInterface;
  i, j, idx, iFile, iSignature, iElement, interval: integer;
  s, sEDID, sFileName: string;
  olLODGen, olDynDOLOD, olRulesAll: TStringList;
  bCounter: boolean;
begin

  if bCreateMod then
    MyMessage('Updating STAT base elements with LOD')
  else
    MyMessage('Building lists of base elements for LODGen and DynDOLOD');

  olLODGen := TStringList.Create;
  olDynDOLOD := TStringList.Create;
  olRulesAll := TStringList.Create;

  olRulesAll.AddStrings(slRulesCache);

  for iFile := 0 to Pred(FileCount) do begin
    if FileIndexToSkip(iFile) then
      Continue;
  
    for iSignature := 0 to Pred(slBaseSignatures.Count) do begin
      group := GroupBySignature(FileByIndex(iFile), slBaseSignatures[iSignature]);

      if ElementCount(group) = 0 then
        Continue;

      MyMessage('    ' + slBaseSignatures[iSignature] + ' ' + IntToStr(ElementCount(group)));

      olLODGen.Clear;
      olDynDOLOD.Clear;
      slRulesCache.Clear;

      sFileName := Lowercase(wbGameName + '-' + RemoveNonAlphaNum(GetFileName(FileByIndex(iFile)))  + '-' + slBaseSignatures[iSignature]);
      if bUseCache then begin
        if LoadOSLFromFile2(slRulesCache, sFileName + '-rules', True, False) then
          if LoadOSLFromFile2(olLODGen, sFileName + '-lodgen', True, False) then
            if LoadOSLFromFile(olDynDOLOD, sFileName + '-dyndolod') then begin
              olSTATDAMT.AddStrings(olLODGen);
              olRulesAll.AddStrings(slRulesCache);

              for i := 0 to Pred(olLODGen.Count) do begin
                TempElement := RecordByLoadOrderFormID(olLODGen.Objects[i]);
                if (Signature(TempElement) = 'STAT') and not GetIsVisibleWhenDistant(TempElement) then begin
                  TempElement := CopyOverrideToFile(TempElement, NEWFile);
                  SetIsVisibleWhenDistant(TempElement, True);
                end;
              end;

              for i := 0 to Pred(olDynDOLOD.Count) do begin
                TempElement := RecordByLoadOrderFormID(olDynDOLOD.Objects[i]);
                sEDID := EditorID(TempElement);
                // DynDOLOD STAT already exits
                idx := olEDIDExists.IndexOf(sEDID + '_DynDOLOD_LOD');
                if  idx <> -1 then
                  olDynDOLOD[i] := IntToStr(olEDIDExists.Objects[idx]);
              end;
              olBaseElementDynDOLOD.AddStrings(olDynDOLOD);

              if olLODGen.Count + olDynDOLOD.Count > 2 then
                MyMessage('      Loaded ' + IntToStr(olLODGen.Count + olDynDOLOD.Count) + ' from cache')
              else
                MyMessage('      no matches');

              Continue;
            end;
      end;
  
      // do it again in case loading failed
      olLODGen.Clear;
      olDynDOLOD.Clear;
      slRulesCache.Clear;

      // add something so that there are always cache files
      olLODGen.AddObject(1, 1);
      olDynDOLOD.AddObject(1, 1);
      slRulesCache.AddObject(1, 1);

      if ElementCount(group) < 100 then
        interval := ElementCount(group)
      else
        interval := ElementCount(group) / 10;
      if ElementCount(group) > 2000 then
        bCounter := True
      else
        bCounter := False;

      for iElement := Pred(ElementCount(group)) downto 0 do begin
        if bCounter then
          if iElement mod interval = 0 then
            MyMessage('      ' + IntToStr(ElementCount(group) - iElement) + ' of ' + IntToStr(ElementCount(group)) + ' done');

        Element := ElementByIndex(group, iElement);
        if ElementType(Element) <> etMainRecord then
          Continue;
        if IsMaster(Element) then begin
          Element := WinningOverride(Element);

          // ignore if in ignore list
          if slModNameIgnore.Count > 0 then
            if wbStringListInString(slModNameIgnore, sCurrentModName) <> -1 then
              Continue;

          sEDID := EditorID(Element);

          if Pos('loadscreen', LowerCase(sEDID)) > 0 then
            Continue;

          // get static LOD string for LODGen
          s := DoBaseElement(Element);

          j := GetRule(Element);

          if Length(s) > 0 then
            olLODGen.AddObject(s, GetLoadOrderFormID(Element));
          
          // DynDOLOD STAT already exits
          if olEDIDExists.IndexOf(sEDID + '_DynDOLOD_LOD') <> -1 then begin
            TempElement := RecordByLoadOrderFormID(olEDIDExists.Objects[olEDIDExists.IndexOf(sEDID + '_DynDOLOD_LOD')]);
            olDynDOLOD.AddObject(GetLoadOrderFormID(TempElement), GetLoadOrderFormID(Element));
            Continue;
          end;

          // If s then there is static LOD which means VWD or Grid rules? Add either to DynDOLOD list
          if (((j and $01000000 = $01000000) and (Length(s) > 0)) 
             or ((j and $01000000 = $00000000) and (Length(s) = 0)))
             and (j shr 12 and $F > 0) then begin
            // create an entry for DynDOLOD
            olDynDOLOD.AddObject(GetLoadOrderFormID(Element), GetLoadOrderFormID(Element));
            AddRuleToCache(GetLoadOrderFormID(Element), True);
            Continue;
          end;

          // add a rule if it is not the all matching '\' for meshes
          AddRuleToCache(GetLoadOrderFormID(Element), False);

        end;
      end;
      olSTATDAMT.AddStrings(olLODGen);
      olBaseElementDynDOLOD.AddStrings(olDynDOLOD);
      olRulesAll.AddStrings(slRulesCache);
      if not bCreateMod then begin
        SaveOSLToFile2(slRulesCache, sFileName + '-rules');
        SaveOSLToFile2(olLODGen, sFileName + '-lodgen');
        SaveOSLToFile(olDynDOLOD, sFileName + '-dyndolod');
      end;
    end;
  end;

  slRulesCache.Clear;
  slRulesCache.AddStrings(olRulesAll);

  olLODGen.Free;
  olDynDOLOD.Free;
  olRulesAll.Free;

  if not bCreateMod then begin
    MyMessage(IntToStr(olSTATDAMT.Count) + ' objects for LODGen');
    MyMessage(IntToStr(olBaseElementDynDOLOD.Count) + ' objects for DynDOLOD');
    MyMessage(IntToStr(slRulesCache.Count) + ' LOD rules');
  end;

  //SaveOSLToFile(slRulesCache, 'rules');
end;

//============================================================================
// find any base element with a matching model 
// STAT and DOOR/ACTI/MSTT/TREE can be used for static LOD just as well
// return string with LOD models for LODGen
function DoBaseElement(Element: IInterface): string;
var
  TempElement: IInterface;
  i, j, idx: integer;
  s, sMODLPath, sMODL, sMODLlookup, sLOD, sLODList, sEDID, sMaterial, m4, m8, m16: string;
  bCheckFilesExist, bSnowMaterial: Boolean;
begin

  Result := '';
  slLODs.Clear;
  sEDID := EditorID(Element);
  bCheckFilesExist := False;
  bSnowMaterial := False;

  // get the first part of the high poly nif file name and try to match it to a LOD file name
  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);

  sMODLlookup := sMODL + s;

  // fix odd combinations
  sMODLlookup := StringReplace(sMODLlookup, 'mossyellow', 'yellow', [rfReplaceAll]);
  sMODLlookup := StringReplace(sMODLlookup, 'mossreach', 'reach', [rfReplaceAll]);
  sMODLlookup := StringReplace(sMODLlookup, 'mosssnow', 'snow', [rfReplaceAll]);
  sMODLlookup := StringReplace(sMODLlookup, 'mosstundra', 'tundra', [rfReplaceAll]);
  sMODLlookup := StringReplace(sMODLlookup, 'tundratundra', 'tundra', [rfReplaceAll]);
  sMODLlookup := StringReplace(sMODLlookup, 'tundrayellow', 'yellow', [rfReplaceAll]);
  sMODLlookup := StringReplace(sMODLlookup, 'tundrasnow', 'snow', [rfReplaceAll]);
  sMODLlookup := StringReplace(sMODLlookup, 'tundracobble', 'cobble', [rfReplaceAll]);

  sLODList := slLODFiles.Values[sMODLlookup];

  // try to guess some more matches
  if (sLODList = '') and (Pos('_snow', sMODL) > 0) then begin
    sMODL := Copy(sMODL, 0, Pos('_snow', sMODL) - 1);
    sLODList := slLODFiles.Values[sMODL];
    // make double sure LOD uses snow shader
    bSnowMaterial := True;
  end;

  // try to guess some more matches
  if (sLODList = '') and (Pos('snow', sMODL) > 1) then begin
    sMODL := Copy(sMODL, 0, Pos('snow', sMODL) - 1);
    sLODList := slLODFiles.Values[sMODL];
    // make double sure LOD uses snow shader
    bSnowMaterial := True;
  end;

  // try to guess some more matches
  if (sLODList = '') then begin
    sMODLlookup := ExtractFilePath(sMODLlookup) + s +  ExtractFileName(sMODLlookup);
    sLODList := slLODFiles.Values[sMODL];
  end;

  // try to guess some more matches
  if (sLODList = '') and (Pos('_snow', sMODL) > 0) then begin
    sMODL := Copy(sMODL, 0, Pos('_snow', sMODL) - 1);
    sLODList := slLODFiles.Values[sMODL];
    // make double sure LOD uses snow shader
    bSnowMaterial := True;
  end;

  // try to guess some more matches
  if (sLODList = '') and (Pos('snow', sMODL) > 1) then begin
    sMODL := Copy(sMODL, 0, Pos('snow', sMODL) - 1);
    sLODList := slLODFiles.Values[sMODL];
    // make double sure LOD uses snow shader
    bSnowMaterial := True;
  end;

  if sLODList = '' then begin
    sLODList := slLODFiles.Values[sMODL];
    // tell about missing variants
    if not bCreateMod and
       (sLODList <> '') and (sMODL <> sMODLlookup) and
       (Pos('road', sMODLlookup) = 0) and
       (Pos('bridge', sMODLlookup) = 0) and
       (Pos('farmhouse', sMODLlookup) = 0)
    then
      // tell about missing variants
      MyMessage('      ' + IntToHex(GetLoadOrderFormID(Element), 8) + ' '+ sEDID + ' looking for ' + sMODLlookup);
  end;

  // check default LOD in case full model didn't match
  if (Length(sLODList) = 0) and ElementExists(Element, 'MNAM') then
    // LOL Bethesda
    if Pos('LargeRainBarrel_Blood01', sEDID) = 0 then begin
      sLODList := LODMeshFor(Element, iLOD4) + ';' + LODMeshFor(Element, iLOD8) + ';' + LODMeshFor(Element, iLOD16);
      bCheckFilesExist := True;
    end;

  // if LOD exists do something with it
  if (Length(sLODList) > 2) then begin

    // add a rule for objects with states for DynDOLOD
    if ElementExists(Element, 'Destructable') then begin
      slRulesCache.AddObject(iGridFarLOD shl 12, GetLoadOrderFormID(Element));
      // no point in adding static LOD for destructables
      exit;
    end;

    // add rules for each matching Model to cache
    idx := AddRuleToCache(GetLoadOrderFormID(Element), True);

    // no rule for this then skip it, very unlikely :)
    if idx = -1 then
      Exit
    else
    j := slRulesCache[idx];

    // VWD rule set?
    if j and $01000000 <> $01000000 then
      Exit;

    // LOD material detection
    if bSnowMaterial then
      sMaterial := 'Snow'
    else
      sMaterial := LODMaterial(Element);

    // allowing/overwriting material ought to be a mesh rule option
    if sMODL = 'windhelmbridge' then
      sMaterial := '';

    // in case of snow MAT try to use the no vertex colors version
    // because the snow shader uses the vertex color to paint the snow cone yellow
    // nvc = no vertex color
    if not (slLODFiles.Values[sMODL + 'nvc'] = '') then begin
      if sMaterial = 'Snow' then begin
        sLODList := slLODFiles.Values[sMODL + 'nvc'];
      end;
      if sMaterial = 'Ash' then begin
        sLODList := slLODFiles.Values[sMODL + 'nvc'];
      end;
    end;

    // use original LOD settings?
    if bUseOrgLOD and ElementExists(Element, 'MNAM') then begin
      m4 := LODMeshFor(Element, j and $F);
      m8 := LODMeshFor(Element, j shr 4 and $F);
      m16 := LODMeshFor(Element, j shr 8 and $F);
      bCheckFilesExist := True;
    end
    else begin
      m4 := LODMeshFromList(Element, sLODList, j and $F);
      m8 := LODMeshFromList(Element, sLODList, j shr 4 and $F);
      m16 := LODMeshFromList(Element, sLODList, j shr 8 and $F);
    end;

    if (m4 <> '') or (m8 <> '') or (m16 <> '') then
      Result := EditorID(Element) + #9 + IntToHex(GetElementNativeValues(Element, 'Record Header\Record Flags') or $00008000, 8) + #9 + sMaterial + #9 + NormalizePath(sMODLPath) + #9 + m4 + #9 + m8 + #9 + m16;
  end;

  // new LODGen.exe with BSA reading support will report missing files
  // warn about missing meshes if MNAM was used
  //if bCheckFilesExist then begin
  //  if m4 <> '' then
  //    if not FileExists(sDataPath + m4) then begin
  //      MyMessage('      missing mesh file ' + sDataPath + m4);
  //      Result := '';
  //    end;  
  //  if m8 <> '' then
  //    if not FileExists(sDataPath + m8) then begin
  //      MyMessage('      missing mesh file ' + sDataPath + m8);
  //      Result := '';
  //    end;
  //  if m16 <> '' then
  //    if not FileExists(sDataPath + m16) then begin
  //      MyMessage('      missing mesh file ' + sDataPath + m16);
  //      Result := '';
  //    end;
  //end;

  if (Result <> '') and (Signature(Element) <> 'TREE') then begin
    // set the VWD flag so full model object shows in loaded cells only
    // setting VWD for trees to add the typical LOD to full model switching delay
    if not GetIsVisibleWhenDistant(Element) then begin
      TempElement := CopyOverrideToFile(Element, NEWFile);
      SetIsVisibleWhenDistant(TempElement, True);
    end;
    // update STAT with LOD when creating a mod for CK or LODGen
    if bCreateMod and not (bUseOrgLOD and ElementExists(Element, 'MNAM')) and (Pos('nolod', Lowercase(sEDID)) = 0) then begin
      TempElement := CopyOverrideToFile(Element, NEWFile);
      TempElement := Add(TempElement, 'MNAM', True);
      if m4 <> '' then
        SetElementEditValues(TempElement, 'LOD #0 (Level 0)\Mesh', StrToHexArray(StringReplace(m4, 'meshes\', '', [rfReplaceAll])));
      if m8 <> '' then
        SetElementEditValues(TempElement, 'LOD #1 (Level 1)\Mesh', StrToHexArray(StringReplace(m8, 'meshes\', '', [rfReplaceAll])));
      if m16 <> '' then
        SetElementEditValues(TempElement, 'LOD #2 (Level 2)\Mesh', StrToHexArray(StringReplace(m16, 'meshes\', '', [rfReplaceAll])));
    end;
  end;

end;

//============================================================================
// process DynDOLOD_manual_STAT.txt
procedure DoManualDynDOLODSTAT;
var
  TempElement: IInterface;
  i: integer;
  sEDID: string;
  slImport, sl: TStringList;
begin

  slImport := TStringList.Create;
  slImport := LoadStringList('manual_base');

  if slImport.Count > 0 then begin
    MyMessage('Add ' + IntToStr(slImport.Count) + ' manual base elements for DynDOLOD');

    sl := TStringList.Create;
    sl.Delimiter := ',';
    sl.StrictDelimiter := True;

    for i := 0 to Pred(slImport.Count) do begin
      sl.CommaText := StringReplace(slImport[i], ', ', ',', [rfReplaceAll]);
      //   0 = signature
      //   1 = EDID
      //   2 = path\to\file.nif org model for lookup
      //   3 = path\to\file.nif lod model to use
      //   4 = mat angle
      //   5 = mat mod file name 
      //   6 = mat form id

      sEDID := RemoveNonAlphaNum(sl[1]) + '_DynDOLOD_LOD';

      if olEDIDExists.IndexOf(sEDID) = -1 then begin
        if sl[5] <> sDynDOLODFileName then
          AddMasterIfMissing(NEWFile, sl[5]);
        TempElement := Add(GroupBySignature(NEWFile, sl[0]), sl[0], True);
        SetElementNativeValues(TempElement, 'EDID', sEDID);
        Add(TempElement, 'Model', True);
        SetElementNativeValues(TempElement, 'Model\MODL', sl[3]);
        SetElementNativeValues(TempElement, 'DNAM\Max Angle (30-120)', StrToFloat(sl[4]));
        SetElementNativeValues(TempElement, 'DNAM\Material', FileNameToLoadOrder(sl[5], StrToInt64('$' + sl[6])));
        olMODLFileBaseElement.AddObject(Lowercase(sl[2]), GetLoadOrderFormID(TempElement));
      end
      else
        olMODLFileBaseElement.AddObject(Lowercase(sl[2]), olEDIDExists.Objects[olEDIDExists.IndexOf(sEDID)]);

    end;
  end;
end;

//============================================================================
// In Tamriel there may be new LOD objects inside the towns worlds
// copy them to Tamriel world space, but be selective about it
// scan the world to add temp REFR for the DOOR/ACTI/MSTT/TREE for LODGen
// scan the world for InitiallyDisabled/EnabledParent for DynDOLOD
// scan the world for neverfades for DynDOLOD
// uses and replaces ExportWorldspace from LODGen.pas so scanning worldspace only needs to be done once
function ScanWorlds(wrld: IInterface): Boolean;
var
  iFile, iElement, counter: integer;
  group, Element, TempElement: IInterface;
  sEDID: string;
begin

  Result := LODGenExportWorldspaceStart(wrld);

  lstSkipDynDOLOD.Clear;
  counter := 0;

  // go through each mod file for more control
  for iFile := 0 to Pred(FileCount) do begin
    if FileIndexToSkip(iFile) then
      Continue;

    // go through each wrld to find child worlds
    group := GroupBySignature(FileByIndex(iFile), 'WRLD');
    for iElement := Pred(ElementCount(group)) downto 0 do begin
      Element := ElementByIndex(group, iElement);
      if ElementType(Element) <> etMainRecord then
        Continue;

      sEDID := EditorID(Element);
      sThisWorld := sEDID;

      // world was done already
      if olEDIDFormID.IndexOf(sThisWorld) <> -1 then
        Continue;

      olEDIDFormID.AddObject(sThisWorld, GetLoadOrderFormID(Element));

      // Prime persistant cell cache
      sThisWorld := sEDID;
      GetCellFromWorldspace(Element, 0, 0, True);

      //if olEDIDExists.IndexOf(sEDID) <> -1 then
      //  Continue;

      // Any world that uses the selected world as parent LOD
      if (Pos(sWorld, GetElementEditValues(Element, 'Parent\WNAM')) > 0) and (GetElementNativeValues(Element, 'Parent\PNAM\Flags') and $2 = $2) then begin
        if slTownsWithFirstborns.IndexOf(sEDID) = -1 then
          slTownsWithFirstborns.Add(sEDID);
        bLODChildOrSelf := True;
      end
      else
        bLODChildOrSelf := False;

      // Only Tamriel/Skyrim has child town worlds, right?
      // Add info to ini for other worlds
      if wbStringListInString(slTownNames, sEDID) <> -1 then begin
        // find town REFER to copy to World
        MyMessage('  Town ' + sEDID);
        lstSkip.Clear;
        lstSkipDynDOLOD.Clear;
        DynDOLODFindREFRs(Element, counter, True);
        inc(counter);
      end
      else begin
        // find world REFR for DynDOLOD, add a REFR to DOOR/ACTI/MSTT/TREE
        if sEDID = sWorld then begin
          MyMessage('  World ' + sEDID);
          lstSkip.Clear;
          lstSkipDynDOLOD.Clear;
          bLODChildOrSelf := True;
          CopyOverrideToFile(Element, NEWFILE);
          DynDOLODFindREFRs(Element, counter, False);
          inc(counter);
        end;
      end;
    end;
  end;

  //if bUpdateDDOL then
  //  MyMessage(IntToStr(olREFRDynDOLOD.Count) + ' references for DynDOLOD');

  //if olREFRTown.Count > 0 then
  //  MyMessage(IntToStr(olREFRTown.Count) + ' references for towns');

  bLODChildOrSelf := True;
end;

//============================================================================
procedure DynDOLODFindREFRs(wrld: IInterface; counter: integer; bTownObjectCopy: Boolean);
var
  l: TList;
  i: integer;
  total, interval: int64;
begin
  l := TList.Create;

  //MyMessage('Gathering references for ' + Name(wrld));
  // bit 0 - skip deleted
  // bit 1 - skip disabled
  // bit 2 - skip XESP
  wbFindREFRsByBase(wrld, slBaseSignatures.Text, 1, l);

  total := l.Count;
  if total < 1000 then
    interval := total
  else
    interval := total / 10;
  if total > 10000 then
    interval := total / 20;

  MyMessage('    Filtering ' + IntToStr(l.Count) + ' references in ' + Name(wrld) + ' for LOD');

  // if update run this will make sure DynDOLODProcessReference does not find stuff added earlier
  sCurrentModName := sDynDOLODFileName;

  for i := 0 to Pred(l.Count) do begin
    if i mod interval = 0 then
      MyMessage('      ' + IntToStr(i) + ' of ' + IntToStr(total) + ' done');
    DynDOLODProcessReference(ObjectToElement(l[i]), bTownObjectCopy);
  end;
  l.Free;
end;

//============================================================================
// bTownObjectCopy = True  - look for new objects with LOD in town world and put them into a stringlist
// bTownObjectCopy = False - scan the world for DOOR/ACTI/MSTT/TREE and create a new REFR to the STAT copy
//                           in case the world is Tamriel clean the town objects stringlist of duplicates
procedure DynDOLODProcessReference(Element: IInterface; bTownObjectCopy: Boolean);
var
  TempElement, BaseElement, BaseElementOrg: IInterface;
  i, j, idxRules, idxLODGen, idxDynDOLOD, rules, rulesREFR: integer;
  FormID, BaseFormID, BaseFormIDOrg: Cardinal;
  s, sEDID, sBaseEDID, sSignature, sLOD, sTriplet, m4, m8, m16, sCustom: string;
  isNeverfade, isLODGen, isDynDOLOD, isDynamic, isCustom, rulesVWD, rulesLOD4, isSelf: boolean;
  Cell: TwbGridCell;
begin

  // fail safe when updating
  if SameText(GetFileName(GetFile(Element)), sCurrentModName) then
    Exit;

  Element := MasterOrSelf(Element);

  // get "real" override record, not one of the ignored mods
  i := Pred(OverrideCount(Element));
  if i > 0 then begin
    s := GetFileName(GetFile(OverrideByIndex(Element, i)));
    while (i > 0) and (wbStringListInString(slModNameIgnore, s) <> -1) do begin
      dec(i);
      s := GetFileName(GetFile(OverrideByIndex(Element, i)));
    end;
    if i >= 0 then
      Element := OverrideByIndex(Element, i)
    else
      Element := WinningOverride(Element);
  end
  else
    Element := WinningOverride(Element);

  FormID := GetLoadOrderFormID(Element);
  sEDID := EditorID(Element);
  BaseElement := WinningOverride(BaseRecord(Element));
  BaseFormID := GetLoadOrderFormID(BaseElement);
  sBaseEDID := EditorID(BaseElement);
  sSignature := Signature(BaseElement);

  // skip markers and stuff
  if BaseFormID < $00000800 then
    Exit;

  // no position or no model
  if not ElementExists(Element, 'DATA') or not ElementExists(BaseElement, 'Model') then
    Exit;

  if GetElementNativeValues(Element, 'Record Header\Record Flags') and $10000 <> $10000 then
    isNeverfade := False
  else
    isNeverfade := True;

  isLODGen := False;
  isDynDOLOD := False;

  // check for a reference rule
  idxRules := slRulesCache.IndexOfObject(FormID);
  if idxRules = -1 then begin
    // fall back to base element rule
    idxRules := slRulesCache.IndexOfObject(BaseFormID);
    // nothing to do if there are no rules, but deal with all neverfades
    if idxRules = -1 then begin
      if not isNeverFade then
        Exit;
      rules := 0;
    end
    else begin
      rules := slRulesCache[idxRules];
      // if rule is zero nothing to do
      if rules = 0 then
        Exit;
    end;
    isCustom := False;
  end
  else begin
    MyMessage('        Rule found for reference ' + IntToHex(FormID, 8));

    isCustom := True;

    rules := slRulesCache[idxRules];
    // if rule is zero nothing to do
    if rules = 0 then
      Exit;

    idxRules := slRulesCache.IndexOfObject(BaseFormID);
    // base element has rules, lets use its LOD settings
    if idxRules <> -1 then begin
      if rules = slRulesCache[idxRules] then begin
        // nothing special to do if reference rule matches base element rule
      end
      else begin
        // get LOD from base element
        idxLODGen := olSTATDAMT.IndexOfObject(BaseFormID);
        if idxLODGen <> -1 then begin
          // why use another freaking StringList when we can do this :)
          s := olSTATDAMT[idxLODGen] + #9;
          j := 0;
          for i := 0 to 5 do begin
            j := Pos(#9, s);
            s := Copy(s, j+1, Length(s));
            if i = 3 then
              m4 := (Copy(s, 0, Pos(#9, s)-1));
            if i = 4 then
              m8 := (Copy(s, 0, Pos(#9, s)-1));
            if i = 5 then
              m16 := (Copy(s, 0, Pos(#9, s)-1));
          end;
          s := m4 + ';' + m8 + ';' + m16;
          m4 := LODMeshFromList(BaseElement, s, rules and $F);
          m8 := LODMeshFromList(BaseElement, s, rules shr 4 and $F);
          m16 := LODMeshFromList(BaseElement, s, rules shr 8 and $F);
        end;
      end;
    end
    // base element has no rules, so there is no LOD, this most likely won't get anything unless full_model is set
    else begin
      m4 := LODMeshFor(BaseElement, rules and $F);
      m8 := LODMeshFor(BaseElement, rules shr 4 and $F);
      m16 := LODMeshFor(BaseElement, rules shr 8 and $F);
    end;

    if (m4 <> '') or (m8 <> '') or (m16 <> '') then begin
      sCustom := EditorID(BaseElement) + #9 + IntToHex(GetElementNativeValues(BaseElement, 'Record Header\Record Flags') or $00008000, 8) + #9 + LODMaterial(BaseElement) + #9 + NormalizePath(Lowercase(GetElementNativeValues(BaseElement, 'Model\MODL'))) + #9 + m4 + #9 + m8 + #9 + m16;
      isLODGen := True;
    end;

    if (((rules and $01000000 = $01000000) and (Length(sCustom) > 0)) or ((rules and $01000000 = $00000000) and (Length(sCustom) = 0))) and (rules shr 12 and $F > 0) then
      isDynDOLOD := True;
 
  end;

  // reference to a TREE in BTT
  if sSignature = 'TREE' then 
    if slREFRTREELOD.IndexOf(FormID) <> -1 then
      Exit;

  // skip filtered ones
  if bTownObjectCopy then 
    if lstSkipDynDOLOD.IndexOf(BaseFormID) <> -1 then
      Exit;

  // this reference only exists in DynDOLOD.esp
  if SameText(GetFileName(GetFile(MasterOrSelf(Element))), sDynDOLODFileName) then
    isSelf := True
  else
    isSelf := False;

  // disabled for good, opposite of player
  if not isSelf and ElementExists(Element, 'XESP') then
    if (GetElementNativeValues(Element, 'XESP\Reference') = 20) and (GetElementNativeValues(Element, 'XESP\Flags') and 1 = 1) then
      Exit;

  // ignore if in ignore list
  if wbStringListInString(slModNameIgnore, GetFileName(GetFile(Element))) <> -1 then
    Exit;

  // position outside of world
  Cell := wbPositionToGridCell(GetPosition(Element));
  if (Cell.x < SkyrimWorld.XMin) or (Cell.x > SkyrimWorld.XMax) or (Cell.y < SkyrimWorld.YMin) or (Cell.y > SkyrimWorld.YMax) then
    Exit;

  // check load order
  if GetLoadOrder(NEWFile) < GetLoadOrder(GetFile(Element)) then begin
    MyMessage('    Ignoring element from ' + GeFileName(GetFile(Element)) + '. Fix the load order or remove the mod. ' + Name(Element));
    exit;
  end;

  // disable this reference - use for LOD without a fullmodel when cell is loaded
  if rules shr 16 and $F = iReferenceDisable then begin
    slREFRToDisable.Add(FormID);
    exit;
  end;

  idxLODGen := olSTATDAMT.IndexOfObject(BaseFormID);
  if (idxLODGen <> -1) and not isCustom then
    isLODGen := True;

  idxDynDOLOD := olBaseElementDynDOLOD.IndexOfObject(BaseFormID);
  if idxDynDOLOD <> -1 and not isCustom then
    isDynDOLOD := True;
 
  if rules and $0000F > 0 then
    rulesLOD4 := True
  else
    rulesLOD4 := False;

  // static or dynamic
  if GetIsInitiallyDisabled(Element) or ElementExists(Element, 'XESP') then
    isDynamic := True
  else
    isDynamic := False;

  // town world object copies
  // this is to match REFR between towns and Tamriel, works so so, some things move position between worlds
  sTriplet := StringReplace(Lowercase(LODMeshFor(BaseElement, iLODFull)), 'lod.nif', '.nif', [rfReplaceAll]) + IntToStr(GetElementNativeValues(Element, 'DATA\Position\X') / 100) + IntToStr(GetElementNativeValues(Element, 'DATA\Position\Y') /100) + IntToStr(GetElementNativeValues(Element, 'DATA\Position\Z') / 100);
  //sTriplet := IntToStr(BaseFormID) + FloatToStr(GetElementNativeValues(Element, 'DATA\Position\X')) + FloatToStr(GetElementNativeValues(Element, 'DATA\Position\Y')) + FloatToStr(GetElementNativeValues(Element, 'DATA\Position\Z');
  // remove the ones from the cache file that are already in Tamriel
  if not bTownObjectCopy then
    if olREFRTown.IndexOf(sTriplet) <> -1 then
      olREFRTown.Delete(olREFRTown.IndexOf(sTriplet));

  // check if LODGen should do it
  if isLODGen and not isDynamic then begin
    if not bTownObjectCopy then begin
      // set neverfades to False since there is static LOD now, if no LOD4 let it be
      if isNeverfade and rulesLOD4 then
        DisableNeverfade(Element, 'static');
      // reference scale
      if ElementExists(Element, 'XSCL') then
        s := GetElementEditValues(Element, 'XSCL')
      else
        s := '1.0';
      if not isCustom then
        sCustom := olSTATDAMT[idxLODGen];
      slExport.Add(Format('%s'#9'%s'#9'%s'#9'%s'#9'%s'#9'%s'#9'%s'#9'%s'#9'%s'#9'%s', [
        IntToHex(GetLoadOrderFormID(Element), 8),
        IntToHex(GetElementNativeValues(Element, 'Record Header\Record Flags'), 8),
        GetElementEditValues(Element, 'DATA\Position\X'),
        GetElementEditValues(Element, 'DATA\Position\Y'),
        GetElementEditValues(Element, 'DATA\Position\Z'),
        GetElementEditValues(Element, 'DATA\Rotation\X'),
        GetElementEditValues(Element, 'DATA\Rotation\Y'),
        GetElementEditValues(Element, 'DATA\Rotation\Z'),
        s,
        sCustom
      ]));

      // just for counting unique LOD objects :)
      if slCache.IndexOfObject(BaseFormID) = -1 then
        slCache.AddObject(1, BaseFormID);
    
      // if no LOD4 let it pass to DynDOLOD for some hybrid LOD
      if rulesLOD4 then
         Exit;
    end;
  end;

  // ignore if a DynDOLOD reference exists already
  if bLODChildOrSelf then begin
    s := RemoveNonAlphaNum(GetFileName(GetFile(MasterOrSelf(Element))) + '_' + IntToHex(FormID and $00FFFFFF, 6) + '_DynDOLOD_LOD');
    if olEDIDExists.IndexOf(s) <> -1 then begin
      // disabled town copies - because reference replace rule
      if isSelf and ElementExists(Element, 'XESP') then
        if (GetElementNativeValues(Element, 'XESP\Reference') = 20) and (GetElementNativeValues(Element, 'XESP\Flags') and 1 = 1) then
          Element := RecordByLoadOrderFormID(olEDIDExists.Objects[olEDIDExists.IndexOf(s)]);
      // add object data for json file
      AddDynDOLODREFERValues(Element, RecordByLoadOrderFormID(olEDIDExists.Objects[olEDIDExists.IndexOf(s)]), BaseRecord(RecordByLoadOrderFormID(olEDIDExists.Objects[olEDIDExists.IndexOf(s)])), BaseElement);
      Exit;
    end;
  end;

  // deal with all other neverfades that have no rules
  if bUpdateDDOL then
    if isNeverFade and (idxRules = -1) then begin
      DisableNeverfade(Element, 'dynamic');
      // add a DynDOLOD base element if needed and add to DynDLOD reference list
      olREFRDynDOLOD.AddObject(AddDynDOLODBaseElement(BaseElement, iGridFarLOD shl 12), FormID);
      // add FarGrid rule for original reference
      slRulesCache.AddObject(iGridFarLOD shl 12, FormID);
      // add to grids
      AddToFirstbornListElement(Element, True);
      // if this is in a town world it needs a parent world copy now that the neverfade flag is not set anymore
      if bTownObjectCopy then
        olREFRTown.AddObject(sTriplet, FormID);
      Exit;
    end;

  // check if this is a DynDOLOD or a static LOD that is dynamic
  if bUpdateDDOL then
    if (isDynDOLOD and not isLODGen) or 
       (isDynDOLOD and isLODGen and isDynamic) or
       (isDynDOLOD and not rulesLOD4)
    then begin
      // add elements world name
      olREFRWorld.AddObject(sThisWorld, FormID);
      if not bTownObjectCopy then begin
        // add a DynDOLOD base element if needed
        BaseElement := RecordByLoadOrderFormID(AddDynDOLODBaseElement(BaseElement, rules));
        // already a neverfade
        if isNeverfade then
          DisableNeverfade(Element, 'DynDOLOD');
        // add to DynDLOD reference list
        olREFRDynDOLOD.AddObject(GetLoadOrderFormID(BaseElement), FormID);
        // add to grids
        AddToFirstbornListElement(Element, True);
        Exit;
      end;
    end;

  // this is a town world
  if bTownObjectCopy then begin
    // town world REFR copies
    // scan the town world and put FID into cache file

    // base choices on updated LOD
    if GetIsVisibleWhenDistant(BaseElement) then
      sLOD := Lowercase(LODMeshFor(BaseElement, iLOD4))
    else
      sLOD := Lowercase(LODMeshFor(BaseElement, iLODFull));

    // reference form id rule trump all else
    if not isCustom then begin
      // reference form ids to ignore from list
      if slChildWorldIgnore.IndexOfObject(FormID) <> -1 then
        Exit;
      // base element editor ids to ignore from ini
      if wbStringListInString(slTownSTATEDIDIgnore, sBaseEDID) <> -1 then begin
        lstSkipDynDOLOD.Add(BaseFormID);
        Exit;
      end;
    end;

    if olREFRTown.IndexOf(sTriplet) = -1 then begin
      // add to list
      olREFRTown.AddObject(sTriplet, FormID);
    end;
  end;
end;

//============================================================================
// from olREFRTown add REFR to Tamriel
procedure AddTownREFR;
var
  Element, TempElement, BaseElement: IInterface;
  BaseFormID: Cardinal;
  i, idx, rules: integer;
  ol: TwbFastStringList;
  sEDID: string;
  Cell: TwbGridCell;
begin
  lstSkip.Clear;
  lstSkipDynDOLOD.Clear;

  // work with a copy because DynDOLODProcessReference deletes double town references from olREFRTown
  // when using it to generate the LOD for the copied records, just didn't feel like adding another boolean
  ol := TwbFastStringList.Create;
  ol.Duplicates := dupIgnore;

  ol.Assign(olREFRTown);

  //MyMessage('Add ' + IntToStr(ol.Count) + ' references from town worlds to ' + sWorld);

  // DynDOLODProcessReference process the added references
  sCurrentModName := '';

  for i := 0 to Pred(ol.Count) do begin

    Element := RecordByLoadOrderFormID(ol.Objects[i]);
    TempElement := Element;
    BaseElement := WinningOverride(BaseRecord(Element));
    BaseFormID := GetLoadOrderFormID(BaseElement);

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

    // do no add again if it already exists
    if olEDIDExists.IndexOf(sEDID) <> -1 then
      TempElement := RecordByLoadOrderFormID(olEDIDExists.Objects[olEDIDExists.IndexOf(sEDID)]);

    // who knew there are commands for this
    Cell := wbPositionToGridCell(GetPosition(Element));

    // add a new reference
    if olEDIDExists.IndexOf(sEDID) = -1 then begin
      TempElement := Add(CopyOverrideToFile(GetCellFromWorldspace(LODGenWorld, Cell.x, Cell.y, False), NEWFile), 'REFR', True);
      CopyREF(Element, TempElement);
      SetElementNativeValues(TempElement, 'EDID', sEDID);
      
      // the copy should not be persistant or a neverfade
      SetElementNativeValues(TempElement, 'Record Header\Record Flags', GetElementNativeValues(Element, 'Record Header\Record Flags') and $FFFEFBFF);

      wbCopyElementToRecord(ElementByPath(Element, 'XESP'), TempElement, True, True);
    end;

    // check for a reference rule and copy it for the new reference copy
    idx := slRulesCache.IndexOfObject(GetLoadOrderFormID(Element));
    if idx <> -1 then
      slRulesCache.AddObject(slRulesCache[idx], GetLoadOrderFormID(TempElement));

    // either new or already existing, do LOD for it
    if TempElement <> Element then begin

      // use full model for creating LOD so rules match
      SetElementNativeValues(TempElement, 'NAME', GetElementNativeValues(Element, 'NAME'));

      // do LOD for this new element
      idx := olREFRWorld.IndexOfObject(GetLoadOrderFormID(Element));
      if idx <> -1 then
        sThisWorld := olREFRWorld[idx]
      else
        sThisWorld := sWorld;

      DynDOLODProcessReference(TempElement, False);

      // use LOD model in the world for game
      idx := olBaseElementDynDOLOD.IndexOfObject(BaseFormID);
      if idx <> -1 then
        SetElementNativeValues(TempElement, 'NAME', olBaseElementDynDOLOD[idx]);

      // for json list
      olSignatureFormID.AddObject(Signature(BaseRecord(TempElement)), GetLoadOrderFormID(TempElement));
    end
    else
      MyMessage('  same element ' + Name(Element));

  end;

  ol.Free;
end;

//============================================================================
// create firstborns for DynDOLOD_manual_refr.txt
procedure DoManualDynDOLODFirstborns;
var
  i: integer;
  slImport, sl: TStringList;
begin
  slImport := TStringList.Create;
  slImport := LoadStringList('manual_refr_' + sWorld);
  if slImport.Count > 0 then begin
    sl := TStringList.Create;
    sl.Delimiter := ',';
    sl.StrictDelimiter := True;
    MyMessage('Add ' + IntToStr(slImport.Count) + ' manual cell for DynDOLOD');
    for i := 0 to Pred(slImport.Count) do begin
      sl.CommaText := StringReplace(slImport[i], ', ', ',', [rfReplaceAll]);
      //   6 = pos x
      //   7 = pos y
      AddToFirstbornListxy(StrToFloat(sl[6]), StrToFloat(sl[7]), True);
    end;
    sl.Free;
  end;
  slImport.Free;
end;

end.
