/* GWSpatialViewer.m
 *  
 * Copyright (C) 2004-2013 Free Software Foundation, Inc.
 *
 * Author: Enrico Sersale <enrico@imago.ro>
 *         Riccardo Mottola
 *
 * Date: June 2004
 *
 * This file is part of the GNUstep Workspace application
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111 USA.
 */

#include <math.h>

#import <AppKit/AppKit.h>
#import "DSStoreInfo.h"
#import "GWViewSettingsManager.h"
#import "GWViewerPrefs.h"
#import "GWSpatialViewer.h"
#import "GWViewersManager.h"
#import "GWViewerWindow.h"
#import "GWViewerScrollView.h"
#import "GWViewerBrowser.h"
#import "GWViewerIconsView.h"
#import "GWSpatialIconsView.h"
#import "GWViewerListView.h"
#import "GWViewerPathsPopUp.h"
#import "GWX11SpatialPath.h"
#import "GWViewer.h"
#import "Workspace.h"
#import "GWFunctions.h"
#import "FSNodeRep.h"
#import "FSNIcon.h"
#import "FSNIconsView.h"
#import "FSNListView.h"
#import "FSNFunctions.h"
#import "NetworkFSNode.h"
#import "Thumbnailer/GWThumbnailer.h"
#import "NetworkServiceManager.h"
 
#define DEFAULT_INCR 150
#define MIN_W_HEIGHT 180

@implementation GWSpatialViewer

- (void)dealloc
{
  [nc removeObserver: self];
  [self teardownDSStoreWatcher];
  
  if (_x11Path) {
    [_x11Path invalidate];
    RELEASE(_x11Path);
  }
  
  RELEASE (baseNode);
  RELEASE (baseNodeArray);
  RELEASE (lastSelection);
  RELEASE (rootViewerKey);
  RELEASE (watchedNodes);
  RELEASE (history);
  RELEASE (vwrwin);
  RELEASE (viewType);
  RELEASE (viewerPrefs);
  RELEASE (dsStoreInfo);
  RELEASE (dsStorePath);
  RELEASE (_settingsManager);
  
  [super dealloc];
}

- (id)initForNode:(FSNode *)node
         inWindow:(GWViewerWindow *)win
         showType:(NSString *)stype
    showSelection:(BOOL)showsel
{
  self = [super init];
  
  if (self) {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];	
    NSString *prefsname;
    id defEntry;

    /* For virtual nodes (like NetworkFSNode), keep the original node.
       For regular paths, create a fresh FSNode from the path. */
    if ([node isKindOfClass:NSClassFromString(@"NetworkFSNode")]) {
      ASSIGN(baseNode, node);
    } else {
      ASSIGN(baseNode, [FSNode nodeWithPath: [node path]]);
    }
    ASSIGN (baseNodeArray, [NSArray arrayWithObject: baseNode]);
    fsnodeRep = [FSNodeRep sharedInstance];
    lastSelection = nil;
    watchedNodes = [NSMutableArray new];
    history = [NSMutableArray new];
    historyPosition = 0;
    manager = [GWViewersManager viewersManager];
    gworkspace = [Workspace gworkspace];
    nc = [NSNotificationCenter defaultCenter];

    // Initialize DS_Store watching
    dsStoreInfo = nil;
    dsStorePath = nil;

    defEntry = [defaults objectForKey: @"browserColsWidth"];
    if (defEntry) {
      resizeIncrement = [defEntry intValue];
    } else {
      resizeIncrement = DEFAULT_INCR;
    }

    rootviewer = ([[baseNode path] isEqual: path_separator()]
                && ([[manager viewersForBaseNode: baseNode] count] == 0));

    if ((rootviewer == NO) && [[baseNode path] isEqual: path_separator()]) {
      rootViewerKey = [manager nextRootViewerKey];
      
      if (rootViewerKey == nil) {
        ASSIGN (rootViewerKey, [NSNumber numberWithUnsignedLong: (unsigned long)self]);
      } else {
        RETAIN (rootViewerKey);
      }
      
      prefsname = GWViewerPrefsKey([node path], YES, rootViewerKey, NO);

    } else {
      rootViewerKey = nil;
      prefsname = GWViewerPrefsKey([node path], YES, nil, NO);
    }

    if ([baseNode isWritable] && (rootviewer == NO) && (rootViewerKey == nil) 
                 && ([[fsnodeRep volumes] containsObject: [node path]] == NO)) {
		  NSString *dictPath = [[baseNode path] stringByAppendingPathComponent: @".gwdir"];

      if ([[NSFileManager defaultManager] fileExistsAtPath: dictPath]) {
        NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile: dictPath];

        if (dict) {
          viewerPrefs = [dict copy];
        }   
      }
    }

    if (viewerPrefs == nil) {
      defEntry = [defaults dictionaryForKey: prefsname];

      if (defEntry == nil) {
        /* One-shot fallback to the pre-split key shared with the browser,
         * so existing users keep their shelf/geometry.  Writes always go
         * to the new spatial key. */
        defEntry = [defaults dictionaryForKey:
          GWViewerLegacySharedPrefsKey([node path], rootViewerKey)];
      }

      if (defEntry) {
        viewerPrefs = [defEntry copy];
      } else {
        viewerPrefs = [NSDictionary new];
      }
    }

    // ================================================================
    // DS_Store Integration - Load view settings (spec §2)
    //   Tier 1: $FOLDER/.DS_Store
    //   Tier 2: per-volume cache
    //   Tier 3: defaults
    // ================================================================
    // Create the view-settings manager with the full read/write hierarchy
    _settingsManager = [[GWViewSettingsManager managerForDirectoryPath:[baseNode path]] retain];

    // Load working copy of view settings via the manager
    // (readSettings returns autoreleased; ASSIGN retains it)
    ASSIGN(dsStoreInfo, [_settingsManager readSettings]);
    if (dsStoreInfo == nil) {
      // Fallback: direct load (shouldn't normally happen)
      ASSIGN(dsStoreInfo, [DSStoreInfo infoForDirectoryPath:[baseNode path]]);
    }

    /* One-time migration off the legacy per-folder .gwdir: fold its view
     * settings into .DS_Store (the single source of truth now) and keep the
     * non-DS_Store remainder (shelf, last selection, geometry) in user
     * defaults, then delete the .gwdir so it is never read or written again. */
    if ([baseNode isWritable] && (rootviewer == NO) && (rootViewerKey == nil)
        && ([[fsnodeRep volumes] containsObject: [baseNode path]] == NO)) {
      NSString *legacyPath = [[baseNode path] stringByAppendingPathComponent: @".gwdir"];
      NSFileManager *fmgr = [NSFileManager defaultManager];
      if ([fmgr fileExistsAtPath: legacyPath]) {
        /* Merge, don't clobber: the just-read .DS_Store wins; the legacy
         * .gwdir only fills fields the .DS_Store does not already have. */
        [dsStoreInfo takeValuesFromViewerPrefs: viewerPrefs
                                preservingExisting: YES];
        [_settingsManager writeSettings: dsStoreInfo];
        [[NSUserDefaults standardUserDefaults] setObject: viewerPrefs
                                                  forKey: prefsname];
        [fmgr removeFileAtPath: legacyPath handler: nil];
        NSDebugLLog(@"gwspace",
          @"GWSpatialViewer: migrated legacy .gwdir at %@ into .DS_Store", legacyPath);
      }
    }

    // Determine view type from DS_Store if available, otherwise use viewerPrefs
    viewType = [viewerPrefs objectForKey: @"viewtype"];
    
    if (dsStoreInfo.loaded && dsStoreInfo.hasViewStyle) {
      NSDebugLLog(@"gwspace", @"╔══════════════════════════════════════════════════════════════════╗");
      NSDebugLLog(@"gwspace", @"║      APPLYING DS_STORE VIEW STYLE                                ║");
      NSDebugLLog(@"gwspace", @"╠══════════════════════════════════════════════════════════════════╣");
      
      viewType = [DSStoreInfo viewTypeNameForViewStyle: dsStoreInfo.viewStyle];
      NSDebugLLog(@"gwspace", @"║ View style from DS_Store: %@", viewType);
      NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
    }

    if (viewType == nil) {
      viewType = @"Icon";
    }
    if (([viewType isEqual: @"Icon"] == NO)
              && ([viewType isEqual: @"List"] == NO)
              && ([viewType isEqual: @"Browser"] == NO)) {
      viewType = @"Icon";
    }

    RETAIN (viewType);

    ASSIGN (vwrwin, win);
    [vwrwin setReleasedWhenClosed: NO];
    [vwrwin setDelegate: self];
    [vwrwin setMinSize: NSMakeSize(resizeIncrement * 2, MIN_W_HEIGHT)];    
    [vwrwin setResizeIncrements: NSMakeSize(resizeIncrement, 1)];

    // ================================================================
    // Apply window geometry from DS_Store (interoperability)
    // ================================================================
    BOOL geometryApplied = NO;
    NSRect dsGeometry = NSZeroRect;  // Declare outside if block for later reference
    
    if (dsStoreInfo.loaded && dsStoreInfo.hasWindowFrame) {
      dsGeometry = [dsStoreInfo gnustepWindowFrameForScreen:[NSScreen mainScreen]];
      
      // Validate geometry
      if (dsGeometry.size.width > 0 && dsGeometry.size.height > 0) {
        NSDebugLLog(@"gwspace", @"╔══════════════════════════════════════════════════════════════════╗");
        NSDebugLLog(@"gwspace", @"║      APPLYING DS_STORE WINDOW GEOMETRY                           ║");
        NSDebugLLog(@"gwspace", @"╠══════════════════════════════════════════════════════════════════╣");
        NSDebugLLog(@"gwspace", @"║ DS_Store content rect: %@", NSStringFromRect(dsStoreInfo.windowFrame));
        NSDebugLLog(@"gwspace", @"║ GNUstep content rect: %@", NSStringFromRect(dsGeometry));
        
        // IMPORTANT: DS_Store stores CONTENT area (excluding titlebar)
        // Convert content rect to full window frame rect
        NSRect windowFrame = [vwrwin frameRectForContentRect:dsGeometry];
        NSDebugLLog(@"gwspace", @"║ Full window frame: %@", NSStringFromRect(windowFrame));
        NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
        
        [vwrwin setFrame:windowFrame display:YES];
        [vwrwin makeKeyAndOrderFront:nil];
        geometryApplied = YES;
      }
    }
    
    if (!geometryApplied) {
      NSDebugLLog(@"gwspace", @"No valid DS_Store geometry, using fallback methods");
      defEntry = [viewerPrefs objectForKey: @"geometry"];
      if (defEntry) {
        [vwrwin setFrameFromString: defEntry];
      } else {
        NSRect r = NSMakeRect(200, 200, resizeIncrement * 3, 300);
        [vwrwin setFrame: rectForWindow([manager viewerWindows], r, YES) 
                 display: NO];
      }
    }
    
    // Log final window frame for verification
    NSRect finalFrame = [vwrwin frame];
    NSDebugLLog(@"gwspace", @"╔══════════════════════════════════════════════════════════════════╗");
    NSDebugLLog(@"gwspace", @"║      FINAL WINDOW FRAME VERIFICATION                          ║");
    NSDebugLLog(@"gwspace", @"╠══════════════════════════════════════════════════════════════════╣");
    if (geometryApplied) {
      NSDebugLLog(@"gwspace", @"║ Source: DS_Store geometry");
      NSDebugLLog(@"gwspace", @"║ DS_Store frame: %@", NSStringFromRect(dsStoreInfo.windowFrame));
      NSDebugLLog(@"gwspace", @"║ Final window frame: %@", NSStringFromRect(finalFrame));
      NSDebugLLog(@"gwspace", @"║ Match: %@", NSEqualRects(dsGeometry, finalFrame) ? @"✓ YES" : @"⚠ NO (may differ due to screen constraints)");
    } else {
      NSDebugLLog(@"gwspace", @"║ Source: Fallback (preferences or default)");
      NSDebugLLog(@"gwspace", @"║ Final window frame: %@", NSStringFromRect(finalFrame));
    }
    NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");

    if (rootviewer) {
      NSString *path = [baseNode path];
      if ([path isEqual: path_separator()]) {
        [vwrwin setTitle: NSLocalizedString(@"System Disk", @"")];
      } else {
        [vwrwin setTitle: [baseNode name]];
      }
    } else {
      /* In spatial mode show only the last path component (node name),
         to be consistent with non-spatial modes. */
      [vwrwin setTitle: [node name]];   
    }

    [self createSubviews];

    if ([viewType isEqual: @"Icon"]) {
      [scroll setHasVerticalScroller: YES];
      nodeView = [[GWSpatialIconsView alloc] initForViewer: self];

    } else if ([viewType isEqual: @"List"]) {
      NSRect r = [[scroll contentView] bounds];
      
      nodeView = [[GWViewerListView alloc] initWithFrame: r forViewer: self];

    } else if ([viewType isEqual: @"Browser"]) {
      [scroll setAutohidesScrollers: NO];
      [scroll setHasHorizontalScroller: YES];
      [scroll setHasVerticalScroller: YES];
      nodeView = [[GWViewerBrowser alloc] initWithBaseNode: baseNode
                                      inViewer: self
		                            visibleColumns: visibleCols
                                      scroller: [scroll horizontalScroller]
                                    cellsIcons: YES
                                 editableCells: YES       
                               selectionColumn: NO];
    }

	  [scroll setDocumentView: nodeView];	
    RELEASE (nodeView);
    [self applyContentBackgroundColor];
    [nodeView showContentsOfNode: baseNode];

    if ([[NSUserDefaults standardUserDefaults] boolForKey: @"use_thumbnails"])
      {
        Thumbnailer *t = [Thumbnailer sharedThumbnailer];
        if (t) [t makeThumbnails: [baseNode path]];
      }

    // ================================================================
    // Apply DS_Store settings AFTER showContentsOfNode loads prefs
    // (This ensures DS_Store settings override loaded preferences)
    // ================================================================
    if ([viewType isEqual: @"Icon"]) {
      [self applyDSStoreSettingsToIconView:nodeView];
    } else if ([viewType isEqual: @"List"]) {
      [self applyDSStoreSettingsToListView:nodeView];
    } else if ([viewType isEqual: @"Browser"]) {
      [self applyDSStoreSettingsToBrowserView:nodeView];
    }

    /*
    * Beeing "spatial", we always set the selection in the browser
    */      
    if (showsel || ([nodeView isSingleNode] == NO)) {
      defEntry = [viewerPrefs objectForKey: @"lastselection"];
    
      if (defEntry) {
        NSFileManager *fm = [NSFileManager defaultManager];
        NSMutableArray *selection = [defEntry mutableCopy];
        int count = [selection count];
        int i;

        for (i = 0; i < count; i++) {
          NSString *s = [selection objectAtIndex: i];

          if ([fm fileExistsAtPath: s] == NO){
            [selection removeObject: s];
            count--;
            i--;
          }
        }

        if ([selection count]) {
          [nodeView selectRepsOfPaths: selection];
        }

        RELEASE (selection);
      }
    }

    [self updeateInfoLabels];

    [self scrollToBeginning];
    
    // Setup DS_Store file watcher for interoperability
    [self setupDSStoreWatcher];
        
    [nc addObserver: self 
           selector: @selector(columnsWidthChanged:) 
               name: @"GWBrowserColumnWidthChangedNotification"
             object: nil];

    /* If this viewer is showing the Network node, observe service changes */
    if ([baseNode isKindOfClass:[NetworkFSNode class]]) {
      [nc addObserver: self
             selector: @selector(networkServicesDidChange:)
                 name: NetworkServicesDidChangeNotification
               object: nil];
    }
    
    invalidated = NO;
    closing = NO;
    
    /* Set the _GW_SPATIAL_PATH X11 atom so the WindowManager can show
     * a path-component popup when the user modifier-clicks on the titlebar. */
    {
      NSString *spatialPath = [baseNode path];
      if (spatialPath) {
        _x11Path = [[GWX11SpatialPath alloc] initWithWindow:vwrwin path:spatialPath];
        NSDebugLLog(@"gwspace", @"GWX11SpatialPath: Set spatial path atom to '%@'", spatialPath);
      }
    }
    
    NSDebugLLog(@"gwspace", @"╔══════════════════════════════════════════════════════════════════╗");
    NSDebugLLog(@"gwspace", @"║      GWSpatialViewer INITIALIZATION COMPLETE                     ║");
    NSDebugLLog(@"gwspace", @"╠══════════════════════════════════════════════════════════════════╣");
    NSDebugLLog(@"gwspace", @"║ Final window frame: %@", NSStringFromRect([vwrwin frame]));
    NSDebugLLog(@"gwspace", @"║ View type: %@", viewType);
    NSDebugLLog(@"gwspace", @"║ DS_Store loaded: %@", dsStoreInfo.loaded ? @"YES" : @"NO");
    if (dsStoreInfo.loaded) {
      NSDebugLLog(@"gwspace", @"║ DS_Store icon positions: %lu", (unsigned long)[[dsStoreInfo filenamesWithPositions] count]);
    }
    NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
  }
  
  return self;
}

- (void)createSubviews
{
  NSRect r = [[vwrwin contentView] bounds];
  float w = r.size.width;  // Width of the window's content view bounds, used for calculating subview positions and sizes
  float h = r.size.height; // Height of the window's content view bounds, used for layout calculations
  int boxh = 16;           // Height of the top box containing the info labels
  int labelw = 106;        // Width of the elements and space labels, determines their horizontal extent
  int labelh = 12;         // Height of the labels, vertically centered within boxh
  int margin = 0;          // Margin spacing around elements (currently zero), controls padding
  unsigned int resizeMask; // Bitmask for autoresizing behavior of subviews, controls how they resize with the window
  BOOL hasScroller;        // Flag indicating if the scroll view should have a vertical scroller, based on view type (Icon/List have it, Browser does not)

  mainView = [[NSView alloc] initWithFrame: r];
  [mainView setAutoresizingMask: (NSViewWidthSizable | NSViewHeightSizable)];

  r = NSMakeRect(0, h - boxh, w, boxh);
  topBox = [[NSView alloc] initWithFrame: r];
  resizeMask = NSViewNotSizable | NSViewWidthSizable | NSViewMinYMargin;
  [topBox setAutoresizingMask: resizeMask];
  [topBox setAutoresizesSubviews: YES];
  [mainView addSubview: topBox];
  RELEASE (topBox);

  // Elements label on the left, vertically centered
  r = NSMakeRect(margin + 4, (boxh - labelh) / 2, labelw, labelh);
  elementsLabel = [[NSTextField alloc] initWithFrame: r];
  [elementsLabel setFont: [NSFont systemFontOfSize: 10]];
  [elementsLabel setAlignment: NSLeftTextAlignment];
  [elementsLabel setDrawsBackground: NO];
  [elementsLabel setTextColor: [NSColor controlShadowColor]];
  [elementsLabel setBezeled: NO]; 
  [elementsLabel setEditable: NO]; 
  [elementsLabel setSelectable: NO];
  resizeMask = NSViewNotSizable | NSViewMaxXMargin;
  [elementsLabel setAutoresizingMask: resizeMask];
  [topBox addSubview: elementsLabel];
  RELEASE (elementsLabel);

  /* The path-component popup has been moved to the window titlebar.
   * The WindowManager reads _GW_SPATIAL_PATH atom and shows the popup
   * when the user modifier-clicks on the title. */

  // Space label on the right, same vertical position as elements label
  r = NSMakeRect(w - labelw - margin - 2 - 4, (boxh - labelh) / 2, labelw, labelh);
  spaceLabel = [[NSTextField alloc] initWithFrame: r];
  [spaceLabel setFont: [NSFont systemFontOfSize: 10]];
  [spaceLabel setAlignment: NSRightTextAlignment];
  [spaceLabel setDrawsBackground: NO];
  [spaceLabel setTextColor: [NSColor controlShadowColor]];
  [spaceLabel setBezeled: NO]; 
  [spaceLabel setEditable: NO]; 
  [spaceLabel setSelectable: NO];
  resizeMask = NSViewNotSizable | NSViewMinXMargin;
  [spaceLabel setAutoresizingMask: resizeMask];
  [topBox addSubview: spaceLabel];
  RELEASE (spaceLabel);

  r = NSMakeRect(margin, 0, w - (margin * 2), h - boxh);
  scroll = [[GWViewerScrollView alloc] initWithFrame: r inViewer: self];
  [scroll setBorderType: NSBezelBorder];
  hasScroller = [viewType isEqual: @"List"];
  [scroll setHasHorizontalScroller: YES];
  [scroll setHasVerticalScroller: hasScroller];
  [scroll setAutohidesScrollers: YES];  // Automatically hide scrollbars when not needed
  resizeMask = NSViewNotSizable | NSViewWidthSizable | NSViewHeightSizable;
  [scroll setAutoresizingMask: resizeMask];
  [mainView addSubview: scroll];
  RELEASE (scroll);

  visibleCols = myrintf(r.size.width / [vwrwin resizeIncrements].width);  
  
  [vwrwin setContentView: mainView];
  RELEASE (mainView);
}

- (FSNode *)baseNode
{
  return baseNode;
}

/* The manager's viewerWillClose: asks any closing viewer for its defaults key
 * to purge prefs of invalid nodes; GWViewer implements this but the spatial
 * viewer never did (unrecognized selector on that path). */
- (NSString *)defaultsKey
{
  return GWViewerPrefsKey([baseNode path], YES, rootViewerKey, NO);
}

- (BOOL)isShowingNode:(FSNode *)anode
{
  return [nodeView isShowingNode: anode];
}

- (BOOL)isShowingPath:(NSString *)apath
{
  return [nodeView isShowingPath: apath];
}

- (void)reloadNodeContents
{
  [nodeView reloadContents];
}

- (void)networkServicesDidChange:(NSNotification *)notification
{
  /* Refresh the network node's subnodes and reload the view */
  if ([baseNode isKindOfClass:[NetworkFSNode class]]) {
    NSDebugLLog(@"gwspace", @"GWSpatialViewer: Network services changed, reloading contents");
    [self reloadNodeContents];
  }
}

- (void)reloadFromNode:(FSNode *)anode
{
  if ([nodeView isShowingNode: anode]) {
    [nodeView reloadFromNode: anode];
    [self updeateInfoLabels];
  }
}

- (void)unloadFromNode:(FSNode *)anode
{
  if ([baseNode isEqual: anode] || [baseNode isSubnodeOfNode: anode]) {
    [self deactivate];
  } else if ([nodeView isShowingNode: anode]) {
    [nodeView unloadFromNode: anode];
  }
}

- (GWViewerWindow *)win
{
  return vwrwin;
}

- (id)nodeView
{
  return nodeView;
}

- (id)shelf
{
  return nil;
}

- (GWViewType)viewType
{
  if ([viewType isEqualToString: @"Browser"]) {
    return GWViewTypeBrowser;
  } else if ([viewType isEqualToString: @"Icon"]) {
    return GWViewTypeIcon;
  } else if ([viewType isEqualToString: @"List"]) {
    return GWViewTypeList;
  }
  return GWViewTypeBrowser;
}

- (BOOL)isRootViewer
{
  return rootviewer;
}

- (NSNumber *)rootViewerKey
{
  return rootViewerKey;
}

- (BOOL)isSpatial
{
  return YES;
}

- (int)vtype
{
  return SPATIAL;
}

- (void)activate
{
  /* Restore delegate — windowWillClose: sets it to nil, so without this
   * windowDidBecomeKey: would never fire when the window is reused. */
  [vwrwin setDelegate: self];

  if ([vwrwin isMiniaturized]) {
    [vwrwin deminiaturize: nil];
  }
  [vwrwin makeKeyAndOrderFront: nil];
  [manager viewer: self didShowNode: baseNode];
}

- (void)deactivate
{
  [vwrwin close];
}

- (void)scrollToBeginning
{
  if ([nodeView isSingleNode]) {
    [nodeView scrollSelectionToVisible];
  }
}

- (void)invalidate
{
  invalidated = YES;
  [self teardownDSStoreWatcher];
}

- (BOOL)invalidated
{
  return invalidated;
}

- (BOOL)isClosing
{
  return closing;
}

- (void)setOpened:(BOOL)opened 
        repOfNode:(FSNode *)anode
{
  id rep = [nodeView repOfSubnode: anode];

  if (rep) {
    [rep setOpened: opened];
    
    if ([nodeView isSingleNode]) { 
      [rep select];
    }
  }
}

- (void)unselectAllReps
{
  [nodeView stopRepNameEditing];
  [nodeView unselectOtherReps: nil];
  [nodeView selectionDidChange];
}

- (void)selectionChanged:(NSArray *)newsel
{
  FSNode *node;
  NSArray *components;

  if (closing) {
    return;
  }
  
  [manager selectionChanged: newsel];

  if (lastSelection && [newsel isEqual: lastSelection]) {
    return;
  }

  ASSIGN (lastSelection, newsel);
  [self updeateInfoLabels]; 
    
  node = [newsel objectAtIndex: 0];   
    
  if ([nodeView isSingleNode]) {    
    if ([node isEqual: baseNode] == NO) {
      return;
    }
  }
 
  if (([node isDirectory] == NO) || [node isPackage] || ([newsel count] > 1)) {
    if ([node isEqual: baseNode] == NO) { // if baseNode is a package 
      node = [FSNode nodeWithPath: [node parentPath]];
    }
  } 
    
  components = [FSNode nodeComponentsFromNode: baseNode toNode: node];

  if ([components isEqual: watchedNodes] == NO) {
    unsigned count = [components count];
    unsigned pos = 0;
    unsigned i;
  
    for (i = 0; i < [watchedNodes count]; i++) { 
      FSNode *nd = [watchedNodes objectAtIndex: i];
      
      if (i < count) {
        FSNode *ndcomp = [components objectAtIndex: i];

        if ([nd isEqual: ndcomp] == NO) {
          [gworkspace removeWatcherForPath: [nd path]];
        } else {
          pos = i + 1;
        }

      } else {
        [gworkspace removeWatcherForPath: [nd path]];
      }
    }

    for (i = pos; i < count; i++) {   
      [gworkspace addWatcherForPath: [[components objectAtIndex: i] path]];
    }

    [watchedNodes removeAllObjects];
    [watchedNodes addObjectsFromArray: components];
  }  
}

- (void)multipleNodeViewDidSelectSubNode:(FSNode *)node
{
  if ([node isDirectory] && ([node isPackage] == NO)) {
    [nodeView setLastShownNode: node];
  }
}

- (void)setSelectableNodesRange:(NSRange)range
{
  visibleCols = range.length;
}

- (void)updeateInfoLabels
{
  NSArray *reps;
  NSString *labelstr;
  NSDictionary *attributes;
  NSNumber *freefs;

  reps = [nodeView reps];
  labelstr = [NSString stringWithFormat: @"%lu ", (reps ? [reps count] : 0)];
  labelstr = [labelstr stringByAppendingString: NSLocalizedString(@"elements", @"")];

  [elementsLabel setStringValue: labelstr];

  attributes = [[NSFileManager defaultManager] fileSystemAttributesAtPath: [[nodeView shownNode] path]];
	freefs = [attributes objectForKey: NSFileSystemFreeSize];

	if (freefs == nil) {  
		labelstr = @"";
	} else {
    unsigned long long freeSize = [freefs unsignedLongLongValue];

    // Remove systemType check as method doesn't exist
    // Default behavior without system-specific adjustments
    // Note: Original code had system-specific size adjustments
    
		labelstr = [NSString stringWithFormat: @"%@ %@", 
                                      sizeDescription(freeSize),
                                            NSLocalizedString(@"free", @"")];
	}

  [spaceLabel setStringValue: labelstr];
}

- (void)popUpAction:(id)sender
{
  NSString *path = [[sender selectedItem] representedObject];

  if ([path isEqual: [baseNode path]] == NO) {
    FSNode *node = [FSNode nodeWithPath: path];
    BOOL close = [sender closeViewer];
    id vwr = nil;
    
    if (close) {
      [pathsPopUp setTarget: nil];
      vwr = self;
    }
  
    [manager viewerOfType: SPATIAL
                 showType: nil
                  forNode: node 
            showSelection: NO
           closeOldViewer: vwr
                 forceNew: NO];
  } else {
    [nodeView showContentsOfNode: baseNode]; 
    [self scrollToBeginning];
    [vwrwin makeFirstResponder: nodeView];
    [manager selectedSpatialViewerChanged: self];
    [manager synchronizeSelectionInParentOfViewer: self];
  }
}

- (BOOL)involvedByFileOperation:(NSDictionary *)opinfo
{
  return [nodeView involvedByFileOperation: opinfo];
}

- (void)nodeContentsWillChange:(NSDictionary *)info
{
  [nodeView nodeContentsWillChange: info];
}
 
- (void)nodeContentsDidChange:(NSDictionary *)info
{
  NSString *operation = [info objectForKey: @"operation"];
  
  /* Handle unmount operations by closing viewers for unmounted paths */
  if ([operation isEqual: @"UnmountOperation"]) {
    NSString *unmountedPath = [info objectForKey: @"unmounted"];
    if (unmountedPath) {
      NSString *viewerPath = [baseNode path];
      /* Close this viewer if it's viewing the unmounted path or any subpath */
      if ([viewerPath isEqual: unmountedPath] || isSubpathOfPath(unmountedPath, viewerPath)) {
        [self deactivate];
        return;
      }
    }
  }
  
  [nodeView nodeContentsDidChange: info];
}

- (void)watchedPathChanged:(NSDictionary *)info
{
  if (invalidated == NO) {
    NSString *path = [info objectForKey: @"path"];
  
    // Check if this is a .DS_Store change
    if (dsStorePath && [path isEqualToString: dsStorePath]) {
      NSDebugLLog(@"gwspace", @"║ DS_Store watcher: File changed, reloading settings");
      [self reapplyDSStoreSettings];
    }
    else if ([nodeView isShowingPath: path]) {
      [nodeView watchedPathChanged: info];
      [self updeateInfoLabels];
    }
  }
}

- (NSArray *)watchedNodes
{
  return watchedNodes;
}

- (void)hideDotsFileChanged:(BOOL)hide
{
  [self reloadFromNode: baseNode];
}

- (void)hiddenFilesChanged:(NSArray *)paths
{
  [self reloadFromNode: baseNode];
}

- (void)columnsWidthChanged:(NSNotification *)notification
{
  NSRect r = [vwrwin frame];
  
  RETAIN (nodeView);  
  [nodeView removeFromSuperviewWithoutNeedingDisplay];
  [scroll setDocumentView: nil];	
  
  resizeIncrement = [(NSNumber *)[notification object] intValue];
  r.size.width = (visibleCols * resizeIncrement);
  [vwrwin setFrame: r display: YES];  
  [vwrwin setMinSize: NSMakeSize(resizeIncrement * 2, MIN_W_HEIGHT)];    
  [vwrwin setResizeIncrements: NSMakeSize(resizeIncrement, 1)];

  [scroll setDocumentView: nodeView];	
  RELEASE (nodeView);
  [self applyContentBackgroundColor];
  [nodeView resizeWithOldSuperviewSize: [nodeView bounds].size];
  [self scrollToBeginning];
}

- (void)applyContentBackgroundColor
{
  NSColor *bg = [NSColor controlBackgroundColor];

  if (scroll) {
    [scroll setBackgroundColor: bg];
    [scroll setDrawsBackground: YES];
  }

  if (nodeView && [nodeView respondsToSelector: @selector(setBackgroundColor:)]) {
    [(id)nodeView setBackgroundColor: bg];
  }
}

- (void)updateDefaults
{
  if ([baseNode isValid]) {
    NSMutableDictionary *updatedprefs = [nodeView updateNodeInfo: NO];
    id defEntry;
    
    if (updatedprefs == nil) {
      updatedprefs = [NSMutableDictionary dictionary];
    }

    [updatedprefs setObject: [NSNumber numberWithBool: YES]
                     forKey: @"spatial"];

    [updatedprefs setObject: [NSNumber numberWithBool: [nodeView isSingleNode]]
                     forKey: @"singlenode"];

    [updatedprefs setObject: viewType forKey: @"viewtype"];

    defEntry = [viewerPrefs objectForKey: @"shelfheight"];
    if (defEntry) {
      [updatedprefs setObject: defEntry forKey: @"shelfheight"];
    }

    defEntry = [viewerPrefs objectForKey: @"shelfdicts"];
    if (defEntry) {
      [updatedprefs setObject: defEntry forKey: @"shelfdicts"];
    }

    defEntry = [nodeView selectedPaths];
    if (defEntry) {
      [updatedprefs setObject: defEntry forKey: @"lastselection"];
    }

    [updatedprefs setObject: [vwrwin stringWithSavedFrame] 
                     forKey: @"geometry"];

    [baseNode checkWritable];

    /* Persist the non-DS_Store remainder (shelf, last selection, geometry
     * fallback) to user defaults; view settings go to .DS_Store below.  The
     * legacy per-folder .gwdir file is no longer written. */
    {
      NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

      [defaults setObject: updatedprefs
                   forKey: GWViewerPrefsKey([baseNode path], YES,
                                            rootViewerKey, NO)];

      /* State now lives under the spatial-specific key; drop the pre-split
       * shared entry so the one-shot read fallback can't later resurrect it. */
      [defaults removeObjectForKey:
        GWViewerLegacySharedPrefsKey([baseNode path], rootViewerKey)];
    }

    // ================================================================
    // Save view settings to .DS_Store (interoperability, spec §3)
    //   Tier 1: Try $FOLDER/.DS_Store (if writable, not policy-blocked)
    //   Tier 2: Fallback to per-volume cache
    //   Tier 3: Clean cache on success
    // ================================================================
    if (_settingsManager) {
      // Update DSStoreInfo from the current preferences
      if (dsStoreInfo == nil) {
        ASSIGN(dsStoreInfo, [DSStoreInfo infoForDirectoryPath:[baseNode path]]);
      }
      [dsStoreInfo takeValuesFromViewerPrefs:updatedprefs];

      // Write via the settings manager
      [_settingsManager writeSettings:dsStoreInfo];
    }

    ASSIGN (viewerPrefs, [updatedprefs makeImmutableCopyOnFail: NO]);
  }
}


- (void)updateWindowTitle
{
  /* Intentionally empty - declared in header but not used in this implementation */
}

//
// DS_Store File Watching Methods
//
- (void)setupDSStoreWatcher
{
  NSString *path = [[baseNode path] stringByAppendingPathComponent:@".DS_Store"];
  
  // Only set up watcher for icon view
  if ([viewType isEqual: @"Icon"] == NO) {
    return;
  }
  
  ASSIGN (dsStorePath, path);
  
  // Check if .DS_Store exists and add filesystem watcher
  // Note: dsStoreInfo was already created in init, don't create it again
  if ([[NSFileManager defaultManager] fileExistsAtPath: dsStorePath]) {
    NSDebugLLog(@"gwspace", @"║ DS_Store watcher: Monitoring %@", dsStorePath);
    
    // Add filesystem watcher for the .DS_Store file
    [gworkspace addWatcherForPath: dsStorePath];
  }
}

- (void)teardownDSStoreWatcher
{
  if (dsStorePath) {
    NSDebugLLog(@"gwspace", @"║ DS_Store watcher: Stopped monitoring");
    [gworkspace removeWatcherForPath: dsStorePath];
    RELEASE (dsStorePath);
    dsStorePath = nil;
  }
}

- (void)reapplyDSStoreSettings
{
  if (!nodeView) {
    return;
  }
  
  // Reload the DS_Store data
  if (!dsStoreInfo) {
    dsStoreInfo = [[DSStoreInfo infoForDirectoryPath: [baseNode path]] retain];
  } else if (![dsStoreInfo reload]) {
    NSDebugLLog(@"gwspace", @"║ DS_Store watcher: Failed to reload DS_Store");
    return;
  }
  
  NSDebugLLog(@"gwspace", @"╔══════════════════════════════════════════════════════════════════╗");
  NSDebugLLog(@"gwspace", @"║      DS_STORE WATCHER: UPDATING VIEW WITH NEW SETTINGS           ║");
  NSDebugLLog(@"gwspace", @"╠══════════════════════════════════════════════════════════════════╣");
  
  // Reapply icon size
  if (dsStoreInfo.hasIconSize && dsStoreInfo.iconSize > 0 && dsStoreInfo.iconSize <= 512) {
    NSDebugLLog(@"gwspace", @"║ Updating icon size: %d", dsStoreInfo.iconSize);
    if ([nodeView respondsToSelector:@selector(setIconSize:)]) {
      [(FSNIconsView *)nodeView setIconSize:dsStoreInfo.iconSize];
    }
  }
  
  // Reapply label position
  if (dsStoreInfo.hasLabelPosition) {
    NSDebugLLog(@"gwspace", @"║ Updating label position: %@", 
          dsStoreInfo.labelPosition == DSStoreLabelPositionBottom ? @"bottom" : @"right");
    if ([nodeView respondsToSelector:@selector(setIconPosition:)]) {
      NSCellImagePosition pos = (dsStoreInfo.labelPosition == DSStoreLabelPositionBottom) 
                                ? NSImageAbove : NSImageLeft;
      [(FSNIconsView *)nodeView setIconPosition:pos];
    }
  }
  
  // Reapply background color
  if (dsStoreInfo.backgroundType == DSStoreBackgroundColor && dsStoreInfo.backgroundColor) {
    NSDebugLLog(@"gwspace", @"║ Updating background color: %@", dsStoreInfo.backgroundColor);
    if ([nodeView respondsToSelector:@selector(setBackgroundColor:)]) {
      [(FSNIconsView *)nodeView setBackgroundColor:dsStoreInfo.backgroundColor];
    }
  }
  
  // Reapply free positioning if available
  if ([dsStoreInfo hasAnyIconPositions]) {
    NSDebugLLog(@"gwspace", @"║ Updating icon positions: %lu items", 
          (unsigned long)[[dsStoreInfo filenamesWithPositions] count]);
    
    NSMutableDictionary *iconPositions = [NSMutableDictionary dictionary];
    for (NSString *filename in [dsStoreInfo filenamesWithPositions]) {
      DSStoreIconInfo *iconInfo = [dsStoreInfo iconInfoForFilename:filename];
      if (iconInfo && iconInfo.hasPosition) {
        [iconPositions setObject:[NSValue valueWithPoint:iconInfo.position] 
                          forKey:filename];
      }
    }
    
    [(FSNIconsView *)nodeView setCustomIconPositions:iconPositions];
  } else {
    NSDebugLLog(@"gwspace", @"║ Free positioning: disabled");
  }
  
  NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
}

- (DSStoreInfo *)dsStoreInfo
{
  return dsStoreInfo;
}

/**
 * Apply DS_Store settings to an icon view.
 * This handles icon size, label position, background, and free icon positioning.
 */
- (void)applyDSStoreSettingsToIconView:(id)iconView
{
  if (!iconView || !dsStoreInfo || !dsStoreInfo.loaded) {
    return;
  }
  
  NSDebugLLog(@"gwspace", @"╔══════════════════════════════════════════════════════════════════╗");
  NSDebugLLog(@"gwspace", @"║      APPLYING DS_STORE ICON VIEW SETTINGS                        ║");
  NSDebugLLog(@"gwspace", @"╠══════════════════════════════════════════════════════════════════╣");
  
  // Apply icon size if available
  if (dsStoreInfo.hasIconSize && dsStoreInfo.iconSize > 0 && dsStoreInfo.iconSize <= 512) {
    NSDebugLLog(@"gwspace", @"║ Setting icon size: %d", dsStoreInfo.iconSize);
    if ([iconView respondsToSelector:@selector(setIconSize:)]) {
      [(FSNIconsView *)iconView setIconSize:dsStoreInfo.iconSize];
      NSDebugLLog(@"gwspace", @"║ Icon size applied. Current: %d", [(FSNIconsView *)iconView iconSize]);
    }
  }
  
  // Apply label position if available
  if (dsStoreInfo.hasLabelPosition) {
    NSDebugLLog(@"gwspace", @"║ Setting label position: %@", 
          dsStoreInfo.labelPosition == DSStoreLabelPositionBottom ? @"bottom" : @"right");
    if ([iconView respondsToSelector:@selector(setIconPosition:)]) {
      NSCellImagePosition pos = (dsStoreInfo.labelPosition == DSStoreLabelPositionBottom) 
                                ? NSImageAbove : NSImageLeft;
      [(FSNIconsView *)iconView setIconPosition:pos];
    }
  }
  
  // Apply grid spacing if available
  if (dsStoreInfo.hasGridSpacing && dsStoreInfo.gridSpacing > 0) {
    NSDebugLLog(@"gwspace", @"║ Setting grid spacing: %.1f", dsStoreInfo.gridSpacing);
  }
  
  // Apply background color if available
  if (dsStoreInfo.backgroundType == DSStoreBackgroundColor && dsStoreInfo.backgroundColor) {
    NSDebugLLog(@"gwspace", @"║ Setting background color: %@", dsStoreInfo.backgroundColor);
    if ([iconView respondsToSelector:@selector(setBackgroundColor:)]) {
      [(FSNIconsView *)iconView setBackgroundColor:dsStoreInfo.backgroundColor];
    }
  }
  
  // Apply background image if available
  if (dsStoreInfo.backgroundType == DSStoreBackgroundPicture && dsStoreInfo.backgroundImagePath) {
    NSDebugLLog(@"gwspace", @"║ Setting background image: %@", dsStoreInfo.backgroundImagePath);
    
    NSImage *bgImage = [[NSImage alloc] initWithContentsOfFile:dsStoreInfo.backgroundImagePath];
    if (bgImage) {
      NSDebugLLog(@"gwspace", @"║   ✓ Background image loaded: %.0fx%.0f", 
            [bgImage size].width, [bgImage size].height);
      if ([iconView respondsToSelector:@selector(setBackgroundImage:)]) {
        [(FSNIconsView *)iconView setBackgroundImage:bgImage];
      }
      [bgImage release];
    } else {
      NSDebugLLog(@"gwspace", @"║   ✗ Failed to load background image from: %@", dsStoreInfo.backgroundImagePath);
    }
  }
  
  // Apply tag colors (lclr) and comments (cmmt) from DS_Store
  NSDictionary *allIconInfo = [dsStoreInfo allIconInfo];
  if (allIconInfo && [allIconInfo count] > 0) {
    NSMutableDictionary *tagColors = [NSMutableDictionary dictionary];
    NSMutableDictionary *comments = [NSMutableDictionary dictionary];
    
    for (NSString *filename in allIconInfo) {
      DSStoreIconInfo *iconInfo = [allIconInfo objectForKey:filename];
      
      // Collect tag colors
      if (iconInfo.hasLabelColor && iconInfo.labelColor != DSStoreLabelColorNone) {
        NSColor *color = [DSStoreIconInfo colorForLabelColor:iconInfo.labelColor];
        if (color) {
          [tagColors setObject:color forKey:filename];
        }
      }
      
      // Collect comments
      if (iconInfo.comments && [iconInfo.comments length] > 0) {
        [comments setObject:iconInfo.comments forKey:filename];
      }
    }
    
    // Apply tag colors
    if ([tagColors count] > 0) {
      NSDebugLLog(@"gwspace", @"║ Applying %lu tag colors", (unsigned long)[tagColors count]);
      if ([iconView respondsToSelector:@selector(setTagColorsFromDictionary:)]) {
        [(FSNIconsView *)iconView setTagColorsFromDictionary:tagColors];
      }
    }
    
    // Apply comments (shown as tooltips)
    if ([comments count] > 0) {
      NSDebugLLog(@"gwspace", @"║ Applying %lu Spotlight comments", (unsigned long)[comments count]);
      if ([iconView respondsToSelector:@selector(setCommentsFromDictionary:)]) {
        [(FSNIconsView *)iconView setCommentsFromDictionary:comments];
      }
    }
  }
  
  NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
}

/**
 * Apply DS_Store settings to a list view.
 * This handles text size, icon size, sort column, and column visibility.
 */
- (void)applyDSStoreSettingsToListView:(id)listView
{
  if (!listView || !dsStoreInfo || !dsStoreInfo.loaded) {
    return;
  }
  
  NSDebugLLog(@"gwspace", @"╔══════════════════════════════════════════════════════════════════╗");
  NSDebugLLog(@"gwspace", @"║      APPLYING DS_STORE LIST VIEW SETTINGS                        ║");
  NSDebugLLog(@"gwspace", @"╠══════════════════════════════════════════════════════════════════╣");
  
  // List view settings from lsvp/lsvP plist:
  // - textSize: Font size for list text
  // - iconSize: Icon size in list rows  
  // - sortColumn: Which column to sort by
  // - columns: Column visibility and widths
  
  // Get the data source which manages the list
  id dataSource = nil;
  if ([listView respondsToSelector:@selector(dataSource)]) {
    dataSource = [listView dataSource];
  }
  
  // Apply sort column if available
  if (dsStoreInfo.hasSortColumn && dsStoreInfo.sortColumn) {
    int infoType = [DSStoreInfo infoTypeForSortColumnName:dsStoreInfo.sortColumn];
    if (infoType >= 0) {
      NSDebugLLog(@"gwspace", @"║ Sort column: %@ -> FSNInfoType %d", dsStoreInfo.sortColumn, infoType);
      if ([dataSource respondsToSelector:@selector(setSortColumn:)]) {
        [(FSNListViewDataSource *)dataSource setSortColumn:(FSNInfoType)infoType];
      }
    } else {
      NSDebugLLog(@"gwspace", @"║ Sort column: %@ (unknown, not applied)", dsStoreInfo.sortColumn);
    }
  }
  
  // Apply list text size if available
  if (dsStoreInfo.hasListTextSize) {
    NSDebugLLog(@"gwspace", @"║ List text size: %d", dsStoreInfo.listTextSize);
    // The list view uses the system font, so we would need to customize the table cells
    // This would require additional FSNListView modifications
  }
  
  // Apply list icon size if available
  if (dsStoreInfo.hasListIconSize) {
    NSDebugLLog(@"gwspace", @"║ List icon size: %d", dsStoreInfo.listIconSize);
    // FSNListView uses fixed-size icons, would need row height adjustment
  }
  
  // Apply column widths if available
  if (dsStoreInfo.columnWidths && [dsStoreInfo.columnWidths count] > 0) {
    NSDebugLLog(@"gwspace", @"║ Column widths from DS_Store:");
    for (NSString *columnName in dsStoreInfo.columnWidths) {
      NSNumber *widthNum = [dsStoreInfo.columnWidths objectForKey:columnName];
      float width = [widthNum floatValue];
      int infoType = [DSStoreInfo infoTypeForSortColumnName:columnName];
      if (infoType >= 0 && [dataSource respondsToSelector:@selector(setColumnWidth:forIdentifier:)]) {
        [dataSource setColumnWidth:width forIdentifier:(FSNInfoType)infoType];
        NSDebugLLog(@"gwspace", @"║   %@ -> width %.0f", columnName, width);
      }
    }
  }
  
  // Apply tag colors and comments to list view items
  NSDictionary *iconInfoDict = [dsStoreInfo allIconInfo];
  if (iconInfoDict && [iconInfoDict count] > 0) {
    NSDebugLLog(@"gwspace", @"║ Applying tag colors/comments to %lu list items", 
          (unsigned long)[iconInfoDict count]);
    // FSNListViewNodeRep doesn't have tagColor/comment support yet
    // This would require additional FSNListViewNodeRep modifications
  }
  
  NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
}

/**
 * Apply DS_Store settings to a browser (column) view.
 * This handles column widths and visibility.
 */
- (void)applyDSStoreSettingsToBrowserView:(id)browserView
{
  if (!browserView || !dsStoreInfo || !dsStoreInfo.loaded) {
    return;
  }
  
  NSDebugLLog(@"gwspace", @"╔══════════════════════════════════════════════════════════════════╗");
  NSDebugLLog(@"gwspace", @"║      APPLYING DS_STORE BROWSER VIEW SETTINGS                     ║");
  NSDebugLLog(@"gwspace", @"╠══════════════════════════════════════════════════════════════════╣");
  
  // Browser view settings from DS_Store:
  // - clw{name}: Column width for named columns
  // - cv{name}: Column visibility
  // - cvlc: Column visibility list
  
  // Apply sidebar width if available (for split view scenarios)
  if (dsStoreInfo.hasSidebarWidth && dsStoreInfo.sidebarWidth > 0) {
    NSDebugLLog(@"gwspace", @"║ Sidebar width from DS_Store: %d", dsStoreInfo.sidebarWidth);
    // Browser view in spatial mode doesn't have a sidebar, but log for reference
  }
  
  // TODO: Apply column widths when we add clw* parsing
  // TODO: Apply column visibility when we add cv* parsing
  
  NSDebugLLog(@"gwspace", @"║ Browser view DS_Store settings: (not yet fully implemented)");
  NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
}

#pragma mark - History Support

- (NSMutableArray *)history
{
  if (!history) {
    history = [NSMutableArray new];
  }
  return history;
}

- (int)historyPosition
{
  return historyPosition;
}

- (void)setHistoryPosition:(int)pos
{
  historyPosition = pos;
}

- (GWViewSettingsManager *)settingsManager
{
  return _settingsManager;
}

@end


//
// GWViewerWindow Delegate Methods
//
@implementation GWSpatialViewer (GWViewerWindowDelegateMethods)

- (void)windowDidBecomeKey:(NSNotification *)aNotification
{
  /* Reload settings from cache — the window may have been reused from a
   * previous session via viewerWithBaseNode: + activate, in which case
   * dsStoreInfo was never re-read.  This ensures label colours, positions,
   * icon sizes, etc. reflect the latest written state. */
  if (_settingsManager)
    {
      DSStoreInfo *fresh = [_settingsManager readSettings];
      if (fresh)
        {
          ASSIGN(dsStoreInfo, fresh);
          /* Re-apply DS_Store settings to update the view */
          if ([viewType isEqual: @"Icon"])
            [self applyDSStoreSettingsToIconView: nodeView];
          else if ([viewType isEqual: @"List"])
            [self applyDSStoreSettingsToListView: nodeView];
          else if ([viewType isEqual: @"Browser"])
            [self applyDSStoreSettingsToBrowserView: nodeView];
        }
    }

  NSArray *selection = [nodeView selectedNodes];
  int count = [selection count];
  
  [vwrwin makeFirstResponder: nodeView]; 

  [manager selectedSpatialViewerChanged: self];

  if (count == 0) {
    selection = [NSArray arrayWithObject: [nodeView shownNode]];
    [manager synchronizeSelectionInParentOfViewer: self];
  
  } else if (count == 1) {
    if (([nodeView isSingleNode] == NO)
             || ([[selection objectAtIndex: 0] isEqual: baseNode])) {
      [manager synchronizeSelectionInParentOfViewer: self];
    }
    
  } else if ([nodeView isSingleNode] == NO) {
    [manager synchronizeSelectionInParentOfViewer: self];
  }
  
  [self selectionChanged: selection];

  if (invalidated == NO) {
    [manager addNode: baseNode toHistoryOfViewer: self];     
    [manager changeHistoryOwner: self];
  }
}

- (BOOL)windowShouldClose:(id)sender
{
  [manager updateDesktop];
	return YES;
}

- (void)windowWillClose:(NSNotification *)aNotification
{
  if (invalidated == NO) {
    closing = YES;
    [self teardownDSStoreWatcher];
    [self updateDefaults];
    [vwrwin setDelegate: nil];
    [manager viewerWillClose: self];
  }
}

- (void)windowWillMiniaturize:(NSNotification *)aNotification
{
  NSImage *image = [fsnodeRep iconOfSize: 48 forNode: baseNode];

  [vwrwin setMiniwindowImage: image];
  [vwrwin setMiniwindowTitle: [baseNode name]];
}

- (void)windowDidResize:(NSNotification *)aNotification
{
  if (nodeView) {
    [nodeView stopRepNameEditing];
    
    if ([nodeView isKindOfClass: [GWViewerBrowser class]]) { 
      [nodeView updateScroller];
    }
  }
}

- (void)openSelectionInNewViewer:(BOOL)newv
{
  if ([[baseNode path] isEqual: [gworkspace trashPath]] == NO) {
    [manager openSelectionInViewer: self closeSender: newv];
  } else {
    NSRunAlertPanel(nil, 
                  NSLocalizedString(@"You can't open a document that is in the Recycler!", @""),
					        NSLocalizedString(@"OK", @""), 
                  nil, 
                  nil);  
  }
}

- (void)openSelectionAsFolder
{
  if ([[baseNode path] isEqual: [gworkspace trashPath]] == NO) {
    [manager openAsFolderSelectionInViewer: self];
  } else {
    NSRunAlertPanel(nil, 
                  NSLocalizedString(@"You can't do this in the Recycler!", @""),
					        NSLocalizedString(@"OK", @""), 
                  nil, 
                  nil);  
  }
}

- (void)openSelectionWith
{
  if ([[baseNode path] isEqual: [gworkspace trashPath]] == NO) {
    [manager openWithSelectionInViewer: self];
  } else {
    NSRunAlertPanel(nil, 
                  NSLocalizedString(@"You can't do this in the Recycler!", @""),
					        NSLocalizedString(@"OK", @""), 
                  nil, 
                  nil);  
  }
}

- (void)newFolder
{
  if ([[baseNode path] isEqual: [gworkspace trashPath]] == NO) {
    [gworkspace newObjectAtPath: [[nodeView shownNode] path] 
                    isDirectory: YES];
  } else {
    NSRunAlertPanel(nil, 
                  NSLocalizedString(@"You can't create a new folder in the Recycler!", @""),
					        NSLocalizedString(@"OK", @""), 
                  nil, 
                  nil);  
  }
}

- (void)newFile
{
  if ([[baseNode path] isEqual: [gworkspace trashPath]] == NO) {
    [gworkspace newObjectAtPath: [[nodeView shownNode] path] 
                    isDirectory: NO];
  } else {
    NSRunAlertPanel(nil, 
                  NSLocalizedString(@"You can't create a new file in the Recycler!", @""),
					        NSLocalizedString(@"OK", @""), 
                  nil, 
                  nil);  
  }
}

- (void)duplicateFiles
{
  if ([[baseNode path] isEqual: [gworkspace trashPath]] == NO) {
    NSArray *selection = [nodeView selectedNodes];

    if (selection && [selection count]) {
      if ([nodeView isSingleNode]) {
        [gworkspace duplicateFiles];
      } else if ([selection isEqual: baseNodeArray] == NO) {
        [gworkspace duplicateFiles];
      }
    }
  } else {
    NSRunAlertPanel(nil, 
                  NSLocalizedString(@"You can't duplicate files in the Recycler!", @""),
					        NSLocalizedString(@"OK", @""), 
                  nil, 
                  nil);  
  }
}

- (void)recycleFiles
{
  if ([[baseNode path] isEqual: [gworkspace trashPath]] == NO) {
    NSArray *selection = [nodeView selectedNodes];

    if (selection && [selection count]) {
      if ([nodeView isSingleNode]) {
        [gworkspace moveToTrash];
      } else if ([selection isEqual: baseNodeArray] == NO) {
        [gworkspace moveToTrash];
      }
    }
  }
}

- (void)emptyTrash
{
  [gworkspace emptyTrash: nil];
}

- (void)deleteFiles
{
  NSArray *selection = [nodeView selectedNodes];

  if (selection && [selection count]) {
    if ([nodeView isSingleNode]) {
      [gworkspace deleteFiles];
    } else if ([selection isEqual: baseNodeArray] == NO) {
      [gworkspace deleteFiles];
    }
  }
}

- (void)goBackwardInHistory
{
  [manager goBackwardInHistoryOfViewer: self];
}

- (void)goForwardInHistory
{
  [manager goForwardInHistoryOfViewer: self];
}

- (void)setViewerBehaviour:(id)sender
{
  [manager setBehaviour: [sender title] forViewer: self];
}

- (void)setViewerType:(id)sender
{
  NSDebugLLog(@"gwspace", @"╔══════════════════════════════════════════════════════════════════╗");
  NSDebugLLog(@"gwspace", @"║ setViewerType: CALLED");
  
  NSString *title = [sender title];
  NSDebugLLog(@"gwspace", @"║ menu title: %@", title);
  NSDebugLLog(@"gwspace", @"║ current viewType: %@", viewType);
  
  // Extract view type from title - handles various localizations
  // Common patterns: "as List", "as Icon", "as Columns", etc.
  // We check for the English keywords anywhere in the string
  NSString *requestedType = nil;
  NSString *upperTitle = [title uppercaseString];
  
  if ([upperTitle rangeOfString:@"BROWSER"].location != NSNotFound ||
      [upperTitle rangeOfString:@"COLUMN"].location != NSNotFound) {
    requestedType = @"Browser";
  } else if ([upperTitle rangeOfString:@"ICON"].location != NSNotFound) {
    requestedType = @"Icon";
  } else if ([upperTitle rangeOfString:@"LIST"].location != NSNotFound) {
    requestedType = @"List";
  }
  
  NSDebugLLog(@"gwspace", @"║ extracted type: %@", requestedType);
  
  if (!requestedType) {
    NSDebugLLog(@"gwspace", @"║ ERROR: Could not determine view type from title!");
    NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
    return;
  }
  
  if (![requestedType isEqualToString:viewType]) {
    NSDebugLLog(@"gwspace", @"║ Switching: %@ -> %@", viewType, requestedType);
    NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
    NSArray *selection = [nodeView selectedNodes];
    NSArray *reps = [nodeView reps];
    NSMutableArray *opennodes = [NSMutableArray array];
    int i;
    
    [nodeView updateNodeInfo: YES];
    if ([nodeView isSingleNode] && ([selection count] == 0)) {
      selection = [NSArray arrayWithObject: [nodeView shownNode]];
    }    
    RETAIN (selection);
    
    for (i = 0; i < [reps count]; i++) {
      id rep = [reps objectAtIndex: i];

      if ([rep isOpened]) {
        [opennodes addObject: [rep node]];
      }
    }

    RETAIN (opennodes);
    
    if ([nodeView respondsToSelector: @selector(releaseScroller)])
      [nodeView releaseScroller];
    [scroll setDocumentView: nil];	
    
    if ([requestedType isEqualToString:@"Browser"]) {
      [scroll setAutohidesScrollers: NO];
      [scroll setHasHorizontalScroller: YES];
      [scroll setHasVerticalScroller: YES];
      nodeView = [[GWViewerBrowser alloc] initWithBaseNode: baseNode
                                      inViewer: self
		                            visibleColumns: visibleCols
                                      scroller: [scroll horizontalScroller]
                                    cellsIcons: YES
                                 editableCells: YES   
                               selectionColumn: NO]; 
      
      ASSIGN (viewType, @"Browser");
      
    } else if ([requestedType isEqualToString:@"Icon"]) {
      nodeView = [[GWSpatialIconsView alloc] initForViewer: self];
      [scroll setHasVerticalScroller: YES];
      ASSIGN (viewType, @"Icon");

    } else if ([requestedType isEqualToString:@"List"]) {
      NSRect r = [[scroll contentView] bounds];

      nodeView = [[GWViewerListView alloc] initWithFrame: r forViewer: self];
      [scroll setHasVerticalScroller: YES];
      ASSIGN (viewType, @"List");
    }

    [scroll setDocumentView: nodeView];
    RELEASE (nodeView);
    [self applyContentBackgroundColor];

    if (nodeView && baseNode) {
      [nodeView showContentsOfNode: baseNode];

      // Reload dsStoreInfo before applying — it may have been updated since init
      if (_settingsManager)
        {
          DSStoreInfo *fresh = [_settingsManager readSettings];
          if (fresh)
            {
              ASSIGN(dsStoreInfo, fresh);
            }
        }

      // Apply DS_Store settings for the new view type
      if ([viewType isEqual: @"Icon"]) {
        [self applyDSStoreSettingsToIconView:nodeView];
      } else if ([viewType isEqual: @"List"]) {
        [self applyDSStoreSettingsToListView:nodeView];
      } else if ([viewType isEqual: @"Browser"]) {
        [self applyDSStoreSettingsToBrowserView:nodeView];
      }
    }
        
    for (i = 0; i < [opennodes count]; i++) {
      id rep = [nodeView repOfSubnode: [opennodes objectAtIndex: i]];

      if (rep) {
        [rep setOpened: YES];
      }
    }
    
    RELEASE (opennodes);
    
    if ([selection count]) {
      [nodeView selectRepsOfSubnodes: selection];
    }
    
    DESTROY (selection);
    
    [self scrollToBeginning];

    [vwrwin makeFirstResponder: nodeView]; 

    for (i = 0; i < [watchedNodes count]; i++) {  
      [gworkspace removeWatcherForPath: [[watchedNodes objectAtIndex: i] path]];
    }
    [watchedNodes removeAllObjects];
    
    DESTROY (lastSelection);
    selection = [nodeView selectedNodes];
    
    if ([selection count] == 0) {
      selection = [NSArray arrayWithObject: [nodeView shownNode]];
    }
    
    [self selectionChanged: selection];
    
    [self updateDefaults];
    
    NSDebugLLog(@"gwspace", @"╔══════════════════════════════════════════════════════════════════╗");
    NSDebugLLog(@"gwspace", @"║ setViewerType: COMPLETED SUCCESSFULLY -> %@", viewType);
    NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
  } else {
    NSDebugLLog(@"gwspace", @"║ Already in %@ view - no switch needed", viewType);
    NSDebugLLog(@"gwspace", @"╚══════════════════════════════════════════════════════════════════╝");
  }
}

- (void)setShownType:(id)sender
{
  NSString *title = [sender title];
  FSNInfoType type = FSNInfoNameType;

  if ([title isEqual: NSLocalizedString(@"Name", @"")]) {
    type = FSNInfoNameType;
  } else if ([title isEqual: NSLocalizedString(@"Type", @"")]) {
    type = FSNInfoKindType;
  } else if ([title isEqual: NSLocalizedString(@"Size", @"")]) {
    type = FSNInfoSizeType;
  } else if ([title isEqual: NSLocalizedString(@"Modification date", @"")]) {
    type = FSNInfoDateType;
  } else if ([title isEqual: NSLocalizedString(@"Owner", @"")]) {
    type = FSNInfoOwnerType;
  } else {
    type = FSNInfoNameType;
  } 

  [(id <FSNodeRepContainer>)nodeView setShowType: type];  
  [self scrollToBeginning];
  [nodeView updateNodeInfo: YES];
}

- (void)setExtendedShownType:(id)sender
{
  [(id <FSNodeRepContainer>)nodeView setExtendedShowType: [sender title]]; 
  [self scrollToBeginning]; 
  [nodeView updateNodeInfo: YES];
}

- (void)setIconsSize:(id)sender
{
  if ([nodeView respondsToSelector: @selector(setIconSize:)]) {
    [(id <FSNodeRepContainer>)nodeView setIconSize: [[sender title] intValue]];
    [self scrollToBeginning];
    [nodeView updateNodeInfo: YES];
  }
}

- (void)setIconsPosition:(id)sender
{
  if ([nodeView respondsToSelector: @selector(setIconPosition:)]) {
    NSString *title = [sender title];
    
    if ([title isEqual: NSLocalizedString(@"Left", @"")]) {
      [(id <FSNodeRepContainer>)nodeView setIconPosition: NSImageLeft];
    } else {
      [(id <FSNodeRepContainer>)nodeView setIconPosition: NSImageAbove];
    }
    
    [self scrollToBeginning];
    [nodeView updateNodeInfo: YES];
  }
}

- (void)setLabelSize:(id)sender
{
  if ([nodeView respondsToSelector: @selector(setLabelTextSize:)]) {
    [nodeView setLabelTextSize: [[sender title] intValue]];
    [self scrollToBeginning];
    [nodeView updateNodeInfo: YES];
  }
}

- (void)chooseLabelColor:(id)sender
{
  NSInteger tag = [sender tag];
  if (tag < 0 || tag > 7) return;

  DSStoreLabelColor labelColor = (DSStoreLabelColor)tag;

  NSArray *selection = [nodeView selectedNodes];
  if (!selection || [selection count] == 0)
    return;

  /* Ensure dsStoreInfo exists */
  if (!dsStoreInfo)
    {
      ASSIGN(dsStoreInfo, [DSStoreInfo infoForDirectoryPath: [baseNode path]]);
    }

  NSString *basePath = [baseNode path];
  if (![basePath hasSuffix: @"/"])
    {
      basePath = [basePath stringByAppendingString: @"/"];
    }

  NSMutableDictionary *tagColors = [NSMutableDictionary dictionary];

  for (FSNode *node in selection)
    {
      if ([node isEqual: baseNode]) continue;

      NSString *nodePath = [node path];
      NSString *filename = [node name];

      /* Get relative path for .DS_Store lookup */
      if ([nodePath hasPrefix: basePath])
        {
          filename = [nodePath substringFromIndex: [basePath length]];
        }

      DSStoreIconInfo *info = [dsStoreInfo iconInfoForFilename: filename];
      if (!info)
        {
          info = [DSStoreIconInfo infoForFilename: filename];
        }
      [info setLabelColor: labelColor];
      [info setHasLabelColor: YES];  /* Always YES so lclr=0 persists as "None" */
      [dsStoreInfo setIconInfo: info forFilename: filename];

      if (labelColor != DSStoreLabelColorNone)
        {
          NSColor *color = [DSStoreIconInfo colorForLabelColor: labelColor];
          if (color)
            {
              [tagColors setObject: color forKey: filename];
            }
        }
    }

  /* Visual feedback — apply tag colors to the icon view */
  if ([nodeView respondsToSelector: @selector(setTagColorsFromDictionary:)])
    {
      [(FSNIconsView *)nodeView setTagColorsFromDictionary: tagColors];
    }

  /* Persist via settings manager (writable → .DS_Store, else → cache) */
  if (_settingsManager)
    {
      [_settingsManager writeSettings: dsStoreInfo];
    }
}

- (void)chooseBackColor:(id)sender
{
  if ([nodeView respondsToSelector: @selector(setBackgroundColor:)]) {
  }
}

- (void)selectAllInViewer
{
	[nodeView selectAll];
}

- (void)showTerminal
{
  NSString *path;

  if ([nodeView isSingleNode]) {
	  path = [[nodeView shownNode] path];
    
  } else {
    NSArray *selection = [nodeView selectedNodes];
    
    if (selection) {
      FSNode *node = [selection objectAtIndex: 0];
      
      if ([selection count] > 1) {
        path = [node parentPath];
        
      } else {
        if ([node isDirectory] && ([node isPackage] == NO)) {
          path = [node path];
      
        } else {
          path = [node parentPath];
        }
      }
    } else {
      path = [[nodeView shownNode] path];
    }
  }

  [gworkspace startXTermOnDirectory: path];
}

- (void)showAttributesInspector:(id)sender
{
  [gworkspace showAttributesInspector: sender];
}

- (NSArray *)lastSelection
{
  return lastSelection;
}

- (BOOL)validateItem:(id)menuItem
{
  SEL action = [menuItem action];
  NSString *actionName = NSStringFromSelector(action);
  NSDebugLLog(@"gwspace", @"validateItem called: action=%@, menuItem=%@", actionName, menuItem);

  // Always enable view type/behaviour items regardless of key window
  if (sel_isEqual(action, @selector(setViewerType:)))
    {
      GWViewType vtype = [self viewType];
      [menuItem setState: ([menuItem tag] == vtype) ? NSOnState : NSOffState];
      return YES;
    }
  if (sel_isEqual(action, @selector(setViewerBehaviour:)))
    {
      int vt = [self vtype];
      [menuItem setState: ([menuItem tag] == vt) ? NSOnState : NSOffState];
      return YES;
    }

  if ([NSApp keyWindow] == vwrwin) {
    SEL action = [menuItem action];
    NSString *itemTitle = [menuItem title];
    NSString *menuTitle = [[menuItem menu] title];

    if ([menuTitle isEqual: NSLocalizedString(@"Icon Size", @"")]) {
      return [nodeView respondsToSelector: @selector(setIconSize:)];
    } else if ([menuTitle isEqual: NSLocalizedString(@"Icon Position", @"")]) {
      return [nodeView respondsToSelector: @selector(setIconPosition:)];
    } else if ([menuTitle isEqual: NSLocalizedString(@"Label Size", @"")]) {
      return [nodeView respondsToSelector: @selector(setLabelTextSize:)];
    } else if ([itemTitle isEqual: NSLocalizedString(@"Label Color...", @"")]) {
      return [nodeView respondsToSelector: @selector(setTextColor:)];
    } else if ([itemTitle isEqual: NSLocalizedString(@"Background Color...", @"")]) {
      return [nodeView respondsToSelector: @selector(setBackgroundColor:)];

    } else if (sel_isEqual(action, @selector(duplicateFiles:))
                    || sel_isEqual(action, @selector(recycleFiles:))
                        || sel_isEqual(action, @selector(deleteFiles:))) {
      if (lastSelection && [lastSelection count]
              && ([lastSelection isEqual: baseNodeArray] == NO)) {
        return ([[baseNode path] isEqual: [gworkspace trashPath]] == NO);
      }

      return NO;

    } else if (sel_isEqual(action, @selector(openSelection:))) {
      if ([[baseNode path] isEqual: [gworkspace trashPath]] == NO) {
        return (lastSelection && [lastSelection count] 
                && ([lastSelection isEqual: baseNodeArray] == NO));
      } 

      return NO;

    } else if (sel_isEqual(action, @selector(openSelectionAsFolder:))) {
      if (lastSelection && ([lastSelection count] == 1)) {  
        return [[lastSelection objectAtIndex: 0] isDirectory];
      }

      return NO;

    } else if (sel_isEqual(action, @selector(openWith:))) {
      BOOL canopen = YES;
      int i;

      if (lastSelection && [lastSelection count]
            && ([lastSelection isEqual: baseNodeArray] == NO)) {
        for (i = 0; i < [lastSelection count]; i++) {
          FSNode *node = [lastSelection objectAtIndex: i];

          if (([node isPlain] == NO) 
                && (([node isPackage] == NO) || [node isApplication])) {
            canopen = NO;
            break;
          }
        }
      } else {
        canopen = NO;
      }

      return canopen;

    } else if (sel_isEqual(action, @selector(newFolder:))
                                || sel_isEqual(action, @selector(newFile:))) {
      if ([[baseNode path] isEqual: [gworkspace trashPath]] == NO) {
        return [[nodeView shownNode] isWritable];
      }

      return NO;
    } else if (sel_isEqual(action, @selector(showAttributesInspector:))) {
      // Get Info requires at least one selected item (not just the base)
      if (lastSelection && [lastSelection count]
            && ([lastSelection isEqual: baseNodeArray] == NO)) {
        return YES;
      }
      return NO;
    }

    return YES;
  } else {
    SEL action = [menuItem action];
    if (sel_isEqual(action, @selector(makeKeyAndOrderFront:))) {
      return YES;
    }
  }
  
  return NO;
}

@end













