
* CPWebView.j
* AppKit
*
* Created by Thomas Robinson.
* Copyright 2008, 280 North, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */



@typedef Window

// FIXME: implement these where possible:
/*
CPWebViewDidBeginEditingNotification = "CPWebViewDidBeginEditingNotification";
CPWebViewDidChangeNotification = "CPWebViewDidChangeNotification";
CPWebViewDidChangeSelectionNotification = "CPWebViewDidChangeSelectionNotification";
CPWebViewDidChangeTypingStyleNotification = "CPWebViewDidChangeTypingStyleNotification";
CPWebViewDidEndEditingNotification = "CPWebViewDidEndEditingNotification";
CPWebViewProgressEstimateChangedNotification = "CPWebViewProgressEstimateChangedNotification";
*/
CPWebViewProgressStartedNotification = "CPWebViewProgressStartedNotification";
CPWebViewProgressFinishedNotification = "CPWebViewProgressFinishedNotification";

CPWebViewScrollAuto = 0;
CPWebViewScrollAppKit = 1;
CPWebViewScrollNative = 2;
CPWebViewScrollNone = 3;

CPWebViewAppKitScrollPollInterval = 1.0;

CPWebViewAppKitScrollMaxPollCount = 3;

@implementation CPWebView : CPView
{
 CPScrollView _scrollView;
 CPView _frameView;

 DOMElement _iframe;
 CPString _mainFrameURL;
 CPArray _backwardStack;
 CPArray _forwardStack;

  BOOL _ignoreLoadStart;
  BOOL _ignoreLoadEnd;
  BOOL _isLoading;
 
  id _downloadDelegate;
  id _frameLoadDelegate;
  id _policyDelegate;
  id _resourceLoadDelegate;
  id _UIDelegate;
 
  CPWebScriptObject _wso;
 
  CPString _url;
  CPString _html;
 
  Function _loadCallback;
 
  int _scrollMode;
  int _effectiveScrollMode;
  BOOL _contentIsAccessible;
  CPTimer _contentSizeCheckTimer;
  int _contentSizePollCount;
 
  int _loadHTMLStringTimer;
 
  BOOL _drawsBackground;
}
 
 

- (id)initWithFrame:(CGRect)frameRect frameName:(CPString)frameName groupName:(CPString)groupName {
  if (self = [self initWithFrame:frameRect]) {
	  _iframe.name = frameName;
  }
  return self;
}



- (id)initWithFrame:(CGRect)aFrame
 {
  if (self = [super initWithFrame:aFrame])
  {
  _mainFrameURL = nil;
  _backwardStack = [];
  _forwardStack = [];
  _scrollMode = CPWebViewScrollAuto;
  _contentIsAccessible = YES;
  _isLoading = NO;
 
  _drawsBackground = YES;
 
  [self setBackgroundColor:[CPColor whiteColor]];
 
  [self _initDOMWithFrame:aFrame];
  }
 
  return self;
}
 
 

- (id)_initDOMWithFrame:(CGRect)aFrame {

  _ignoreLoadStart = YES;
  _ignoreLoadEnd = YES;
 
  _iframe = document.createElement("iframe");
  _iframe.name = "iframe_" + FLOOR(RAND() * 10000);
  _iframe.style.width = "100%";
  _iframe.style.height = "100%";
  _iframe.style.borderWidth = "0px";
  _iframe.frameBorder = "0";
 
  [self _applyBackgroundColor];
 
  _loadCallback = function() {
  // HACK: this block handles the case where we don't know about loads initiated by the user clicking a link
  if (!_ignoreLoadStart) {
  // post the start load notification
  [self _startedLoading];
 
  if (_mainFrameURL)
	  [_backwardStack addObject:_mainFrameURL];
 
	  // FIXME: this doesn't actually get the right URL for different domains. Not possible due to browser security restrictions.
	  _mainFrameURL = _iframe.src;
 
	  // clear the forward
	  [_forwardStack removeAllObjects];
  }
  else
	  _ignoreLoadStart = NO;
 
  if (!_ignoreLoadEnd) {
	  [self _finishedLoading];
  }
  else
	  _ignoreLoadEnd = NO;
 
  [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
  };
 
  if (_iframe.addEventListener)
	  _iframe.addEventListener("load", _loadCallback, false);
  else if (_iframe.attachEvent)
	  _iframe.attachEvent("onload", _loadCallback);
 
  _frameView = [[CPView alloc] initWithFrame:[self bounds]];
  [_frameView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
 
  _scrollView = [[CPScrollView alloc] initWithFrame:[self bounds]];
  [_scrollView setAutohidesScrollers:YES];
  [_scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
  [_scrollView setDocumentView:_frameView];
 
  _frameView._DOMElement.appendChild(_iframe);
 
  [self _updateEffectiveScrollMode];
 
  [self addSubview:_scrollView];
}
 
 

- (void)setFrameSize:(CGSize)aSize
 {
  [super setFrameSize:aSize];
  [self _resizeWebFrame];
}
 
 

- (void)viewDidUnhide
 {
  // Sizing cannot properly happen while we're hidden because the iframe is inaccessible.
  // So now that it is accessible again, make sure to catch up.
  [_frameView setFrameSize:[_scrollView contentSize]];
  [self _resizeWebFrame];
  [self _scheduleContentSizeCheck];
}
 
 

- (void)_attachScrollEventIfNecessary
 {
  if (_effectiveScrollMode !== CPWebViewScrollAppKit)
  return;
 
  var win = [self DOMWindow];
 
  if (win && win.addEventListener)
  {
  var scrollEventHandler = function(anEvent)
  {
  var frameBounds = [self bounds],
  frameCenter = CGPointMake(CGRectGetMidX(frameBounds), CGRectGetMidY(frameBounds)),
  windowOrigin = [self convertPoint:frameCenter toView:nil],
  globalOrigin = [[self window] convertBaseToBridge:windowOrigin];
 
  anEvent._overrideLocation = globalOrigin;
  [[[self window] platformWindow] scrollEvent:anEvent];
  };
 
  win.addEventListener("DOMMouseScroll", scrollEventHandler, false);
  win.addEventListener("wheel", scrollEventHandler, false);
  }
}
 
 

- (void)_resizeWebFrame
 {
  // When a webview is not in the DOM we can't inspect its contents for sizing information.
  // If we try, we might end up setting the fallback frame size which will then become
  // somewhat sticky.
  if (![self _isVisible])
  {
  return;
  }
 
  if (_effectiveScrollMode === CPWebViewScrollAppKit)
  {
  var visibleRect = [_frameView visibleRect];
  [_frameView setFrameSize:CGSizeMake(CGRectGetMaxX(visibleRect), CGRectGetMaxY(visibleRect))];
 
  // try to get the document size so we can correctly set the frame
  var win = [self DOMWindow];
 
  if (win && win.document && win.document.body)
  {
  var width = win.document.body.scrollWidth,
  height = win.document.body.scrollHeight;
 
  _iframe.setAttribute("width", width);
  _iframe.setAttribute("height", height);
 
  [_frameView setFrameSize:CGSizeMake(width, height)];
  }
  else
  {
  // If we do have access to the content, it might be that the 'body' element simply hasn't loaded yet.
  // The size will be updated by the content size timer in this case.
  if (!win || !win.document)
  {
  CPLog.warn("using default size 800*1600");
  [_frameView setFrameSize:CGSizeMake(800, 1600)];
  }
  }
 
  [_frameView scrollRectToVisible:visibleRect];
  }
}

- (void)setScrollMode:(int)aScrollMode
 {
  if (_scrollMode == aScrollMode)
  return;
 
  _scrollMode = aScrollMode;
 
  [self _updateEffectiveScrollMode];
}

- (int)effectiveScrollMode
 {
  return _effectiveScrollMode;
}

- (void)_updateEffectiveScrollMode
 {
  var _newScrollMode = CPWebViewScrollAppKit;
 
  if (_scrollMode == CPWebViewScrollNative
  || (_scrollMode == CPWebViewScrollAuto && !_contentIsAccessible)
  || CPBrowserIsEngine(CPInternetExplorerBrowserEngine))
  {
  _newScrollMode = CPWebViewScrollNative;
  }
  else if (_scrollMode == CPWebViewScrollAppKit && !_contentIsAccessible)
  {
  // Same behaviour as the previous case except that a warning is logged when AppKit
  // scrollers can't be used.
  CPLog.warn(self + " unable to use CPWebViewScrollAppKit scroll mode due to same origin policy.");
  _newScrollMode = CPWebViewScrollNative;
  }
 
  if (_newScrollMode !== _effectiveScrollMode)
  [self _setEffectiveScrollMode:_newScrollMode];
}

- (void)_setEffectiveScrollMode:(int)aScrollMode
 {
  _effectiveScrollMode = aScrollMode;
 
  _ignoreLoadStart = YES;
  _ignoreLoadEnd = YES;
 
  var parent = _iframe.parentNode;
  // FIXME "scrolling" can't be changed without readding the iframe. Unfortunately this causes a reload.
  parent.removeChild(_iframe);
 
  if (_effectiveScrollMode === CPWebViewScrollAppKit)
  {
  [_scrollView setHasHorizontalScroller:YES];
  [_scrollView setHasVerticalScroller:YES];
 
  _iframe.setAttribute("scrolling", "no");
  }
  else if (_effectiveScrollMode === CPWebViewScrollNone)
  {
  [_scrollView setHasHorizontalScroller:NO];
  [_scrollView setHasVerticalScroller:NO];
 
  _iframe.setAttribute("scrolling", "no");
  }
  else
  {
  [_scrollView setHasHorizontalScroller:NO];
  [_scrollView setHasVerticalScroller:NO];
 
  _iframe.setAttribute("scrolling", "auto");
 
  [_frameView setFrameSize:[_scrollView bounds].size];
  }
 
  parent.appendChild(_iframe);
  [self _applyBackgroundColor];
 
  [self _resizeWebFrame];
}
 
 

- (void)_maybePollWebFrameSize
 {
  if (CPWebViewAppKitScrollMaxPollCount == 0 || _contentSizePollCount++ < CPWebViewAppKitScrollMaxPollCount)
  [self _resizeWebFrame];
  else
  [_contentSizeCheckTimer invalidate];
}
 
 

- (void)loadHTMLString:(CPString)aString
 {
  [self loadHTMLString:aString baseURL:nil];
}
 
 

- (void)loadHTMLString:(CPString)aString baseURL:(CPURL)URL
 {
  // FIXME: do something with baseURL?
  [_frameView setFrameSize:[_scrollView contentSize]];
 
  [self _startedLoading];
 
  _ignoreLoadStart = YES;
 
  _url = nil;
  _html = aString;
 
  [self _load];
}

- (void)_loadMainFrameURL
 {
  [self _startedLoading];
 
  _ignoreLoadStart = YES;
 
  _url = _mainFrameURL;
  _html = nil;
 
  [self _load];
}

- (void)_load {
  if (_url) {
	  // Try to figure out if this URL will pass the same origin policy and hence allow us to potentially
	  // use appkit scrollbars.
	  var cpurl = [CPURL URLWithString:_url];
	  _contentIsAccessible = [cpurl _passesSameOriginPolicy];
	  [self _updateEffectiveScrollMode];
 
	  _ignoreLoadEnd = NO;
 
	  _iframe.src = _url;
  }
  else if (_html !== nil) {
	  // clear the iframe
	_iframe.src = "";
 
	  _contentIsAccessible = YES;
	  [self _updateEffectiveScrollMode];
 
	  _ignoreLoadEnd = NO;
 
	  if (_loadHTMLStringTimer !== nil) {
		  window.clearTimeout(_loadHTMLStringTimer);
		  _loadHTMLStringTimer = nil;
	  }
 
	  // need to give the browser a chance to reset iframe, otherwise we'll be document.write()-ing the previous document
	  _loadHTMLStringTimer = window.setTimeout(function()
		  {
		  var win = [self DOMWindow];
 
		  /*
		  If _html is the empty string, subtitute in an empty HTML structure. Just leaving the contents entirely empty prompts the browser to subtitute in a white page which would interfere with any custom background colours in use by this web view.
		  */
		  if (win)
		  win.document.write(_html || "<html><body></body></html>");
 
		  window.setTimeout(_loadCallback, 1);
		  }, 0);
	}
}



- (void)_startedLoading {
  _isLoading = YES;
 
  [[CPNotificationCenter defaultCenter] postNotificationName:CPWebViewProgressStartedNotification object:self];
 
  if ([_frameLoadDelegate respondsToSelector:@selector(webView:didStartProvisionalLoadForFrame:)])
  [_frameLoadDelegate webView:self didStartProvisionalLoadForFrame:nil]; // FIXME: give this a frame somehow?
}

- (void)_finishedLoading
 {
  _isLoading = NO;
 
  [self _resizeWebFrame];
  [self _attachScrollEventIfNecessary];
 
  [self _scheduleContentSizeCheck];
 
  [[CPNotificationCenter defaultCenter] postNotificationName:CPWebViewProgressFinishedNotification object:self];
 
  if ([_frameLoadDelegate respondsToSelector:@selector(webView:didFinishLoadForFrame:)])
  [_frameLoadDelegate webView:self didFinishLoadForFrame:nil]; // FIXME: give this a frame somehow?
}

- (void)_scheduleContentSizeCheck
 {
  [_contentSizeCheckTimer invalidate];
  if (_effectiveScrollMode == CPWebViewScrollAppKit)
  {
  /*
  FIXME Need better method.
  We don't know when the content of the iframe changes size (e.g. a
  picture finishes loading, dynamic content is loaded). Often when a
  page has initially 'loaded', it does not yet have its final size. In
  lieu of any resize events we will simply check back in a few times
  some time after loading.
 
  We run these checks only a limited number of times as to not deplete
  battery life and slow down the software needlessly. This does mean
  there are situations where the content changes size and the AppKit
  scrollbars will be out of sync. Users who have dynamic content
  in their web view will, for now, have to implement domain specific
  fixes.
  */
 
  _contentSizePollCount = 0;
  _contentSizeCheckTimer = [CPTimer scheduledTimerWithTimeInterval:CPWebViewAppKitScrollPollInterval target:self selector:@selector(_maybePollWebFrameSize) userInfo:nil repeats:YES];
  }
}

- (BOOL)isLoading
 {
  return _isLoading;
}

- (CPString)mainFrameURL
 {
  return _mainFrameURL;
}

- (void)setMainFrameURL:(CPString)URLString
 {
  if (_mainFrameURL)
  [_backwardStack addObject:_mainFrameURL];
 
  _mainFrameURL = URLString;
  [_forwardStack removeAllObjects];
 
  [self _loadMainFrameURL];
}

- (BOOL)goBack
 {
  if (_backwardStack.length > 0)
  {
  if (_mainFrameURL)
  [_forwardStack addObject:_mainFrameURL];
 
  _mainFrameURL = [_backwardStack lastObject];
  [_backwardStack removeLastObject];
 
  [self _loadMainFrameURL];
 
  return YES;
  }
 
  return NO;
}
 
 

- (BOOL)goForward
 {
  if (_forwardStack.length > 0)
  {
  if (_mainFrameURL)
  [_backwardStack addObject:_mainFrameURL];
 
  _mainFrameURL = [_forwardStack lastObject];
  [_forwardStack removeLastObject];
 
  [self _loadMainFrameURL];
 
  return YES;
  }
 
  return NO;
}
 
 

- (BOOL) canGoBack {
  return (_backwardStack.length > 0);
}
 
 

- (BOOL)canGoForward {
  return (_forwardStack.length > 0);
}
 
 

- (WebBackForwardList)backForwardList {
  // FIXME: return a real WebBackForwardList?
  return { back: _backwardStack, forward: _forwardStack };
}
 
 

- (void)close {
  _iframe.parentNode.removeChild(_iframe);
}
 
 

- (DOMWindow)DOMWindow {
	try {
		return _iframe.contentWindow || (_iframe.contentDocument && _iframe.contentDocument.defaultView);
	}
	catch (e) {
		return nil;
	}
}
 
 

- (CPWebScriptObject)windowScriptObject {
  var win = [self DOMWindow];
 
  if (!_wso || win != [_wso window]) {
  if (win)
  _wso = [[CPWebScriptObject alloc] initWithWindow:win];
  else
  _wso = nil;
  }
 
  return _wso;
}
 
 

- (CPString)stringByEvaluatingJavaScriptFromString:(CPString)script
 {
  var result = [self objectByEvaluatingJavaScriptFromString:script];
  return result ? String(result) : nil;
}

- (JSObject)objectByEvaluatingJavaScriptFromString:(CPString)script
 {
  return [[self windowScriptObject] evaluateWebScript:script];
}

- (DOMCSSStyleDeclaration)computedStyleForElement:(DOMElement)element pseudoElement:(CPString)pseudoElement
 {
  var win = [[self windowScriptObject] window];
 
  if (win)
  {
  // FIXME: IE version?
  return win.document.defaultView.getComputedStyle(element, pseudoElement);
  }
  return nil;
}
 

- (BOOL)drawsBackground
 {
  return _drawsBackground;
}

- (void)setDrawsBackground:(BOOL)drawsBackground
 {
  if (drawsBackground == _drawsBackground)
  return;
 
  _drawsBackground = drawsBackground;
 
  [self _applyBackgroundColor];
}

- (void)setBackgroundColor:(CPColor)aColor
 {
  [super setBackgroundColor:aColor];
  [self _applyBackgroundColor];
}

- (void)_applyBackgroundColor
 {
  if (_iframe)
  {
  var bgColor = [self backgroundColor] || [CPColor whiteColor];
  _iframe.allowtransparency = !_drawsBackground;
  _iframe.style.backgroundColor = _drawsBackground ? [bgColor cssString] : "transparent";
  }
}
 
 // IBActions

- (@action)takeStringURLFrom:(id)sender
 {
  [self setMainFrameURL:[sender stringValue]];
}

- (@action)goBack:(id)sender
 {
  [self goBack];
}

- (@action)goForward:(id)sender
 {
  [self goForward];
}

- (@action)stopLoading:(id)sender
 {
  // FIXME: what to do?
}

- (@action)reload:(id)sender
 {
  // If we're displaying pure HTML, redisplay it.
  if (!_url && (_html !== nil))
  [self loadHTMLString:_html];
  else
  [self _loadMainFrameURL];
}

- (@action)print:(id)sender
 {
  try
  {
  [self DOMWindow].print();
  }
  catch (e)
  {
  alert('Please click the webpage and select "Print" from the "File" menu');
  }
}
 
 
 // Delegates:
 
 // FIXME: implement more delegates, though most of these will likely never work with the iframe implementation

- (id)downloadDelegate
 {
  return _downloadDelegate;
}

- (void)setDownloadDelegate:(id)anObject
 {
  _downloadDelegate = anObject;
}

- (id)frameLoadDelegate
 {
  return _frameLoadDelegate;
}

- (void)setFrameLoadDelegate:(id)anObject
 {
  _frameLoadDelegate = anObject;
}

- (id)policyDelegate
 {
  return _policyDelegate;
}

- (void)setPolicyDelegate:(id)anObject
 {
  _policyDelegate = anObject;
}

- (id)resourceLoadDelegate
 {
  return _resourceLoadDelegate;
}

- (void)setResourceLoadDelegate:(id)anObject
 {
  _resourceLoadDelegate = anObject;
}

- (id)UIDelegate
 {
  return _UIDelegate;
}

- (void)setUIDelegate:(id)anObject
 {
  _UIDelegate = anObject;
}
 
 @end
 
 @implementation CPWebScriptObject : CPObject
 {
  Window _window;
}

- (id)initWithWindow:(Window)aWindow
 {
  if (self = [super init])
  {
  _window = aWindow;
  }
 
  return self;
}

- (id)callWebScriptMethod:(CPString)methodName withArguments:(CPArray)args
 {
  // Would using "with" be better here?
  if (typeof _window[methodName] == "function")
  {
  try
  {
  return _window[methodName].apply(_window, args);
  }
  catch (e)
  {
  }
  }
  return undefined;
}

- (id)evaluateWebScript:(CPString)script
 {
  try
  {
  return _window.eval(script);
  }
  catch (e)
  {
  // FIX ME: if we fail inside here, shouldn't we return an exception?
  }
 
  return undefined;
}

- (Window)window
 {
  return _window;
}
 
 
@end


@implementation CPWebView (CPCoding)
chartPlot: (id)initWithCoder:(CPCoder)aCoder
{
 self = [super initWithCoder:aCoder];

 if (self)
 {
 // FIXME: encode/decode these?
 _mainFrameURL = nil;
 _backwardStack = [];
 _forwardStack = [];
 _scrollMode = CPWebViewScrollAuto;

#if PLATFORM(DOM)
 [self _initDOMWithFrame:[self frame]];
#endif

 if (![self backgroundColor])
 [self setBackgroundColor:[CPColor whiteColor]];

 [self _updateEffectiveScrollMode];
}

 return self;
}
chartPlot: (void)encodeWithCoder:(CPCoder)aCoder
{
 var actualSubviews = _subviews;
 _subviews = [];
 [super encodeWithCoder:aCoder];
 _subviews = actualSubviews;
}

@end



@implementation CPURL(SOP)
chartPlot: (BOOL)_passesSameOriginPolicy
{
 var documentURL = [CPURL URLWithString:window.location.href];

 if ([documentURL isFileURL] && CPFeatureIsCompatible(CPSOPDisabledFromFileURLs))
 return YES;

 // Relative URLs always pass the SOP.
 if (![self scheme] && ![self host] && ![self port])
 return YES;

 return ([documentURL scheme] == [self scheme] && [documentURL host] == [self host] && [documentURL port] == [self port]);
}

@end


