/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * The contents of this file are subject to the Netscape Public License
 * Version 1.0 (the "NPL"); you may not use this file except in
 * compliance with the NPL.  You may obtain a copy of the NPL at
 * http://www.mozilla.org/NPL/
 *
 * Software distributed under the NPL is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
 * for the specific language governing rights and limitations under the
 * NPL.
 *
 * The Initial Developer of this code under the NPL is Netscape
 * Communications Corporation.  Portions created by Netscape are
 * Copyright (C) 1998 Netscape Communications Corporation.  All Rights
 * Reserved.
 */
#include "nsIPresShell.h"
#include "nsIPresContext.h"
#include "nsIContent.h"
#include "nsIContentDelegate.h"
#include "nsIDocument.h"
#include "nsIStyleSet.h"
#include "nsIStyleContext.h"
#include "nsIFrame.h"
#include "nsReflowCommand.h"
#include "nsIViewManager.h"
#include "nsIView.h"
#include "nsCRT.h"
#include "plhash.h"

#undef NOISY

static PLHashNumber HashKey(nsIFrame* key)
{
  return (PLHashNumber) key;
}

static PRIntn CompareKeys(nsIFrame* key1, nsIFrame* key2)
{
  return key1 == key2;
}

class FrameHashTable {
public:
  FrameHashTable();
  ~FrameHashTable();

  void* Get(nsIFrame* aKey);
  void* Put(nsIFrame* aKey, void* aValue);
  void* Remove(nsIFrame* aKey);

protected:
  PLHashTable* mTable;
};

FrameHashTable::FrameHashTable()
{
  mTable = PL_NewHashTable(8, (PLHashFunction) HashKey,
                           (PLHashComparator) CompareKeys,
                           (PLHashComparator) nsnull,
                           nsnull, nsnull);
}

FrameHashTable::~FrameHashTable()
{
  // XXX if debugging then we should assert that the table is empty
  PL_HashTableDestroy(mTable);
}

/**
 * Get the data associated with a frame.
 */
void* FrameHashTable::Get(nsIFrame* aKey)
{
  PRInt32 hashCode = (PRInt32) aKey;
  PLHashEntry** hep = PL_HashTableRawLookup(mTable, hashCode, aKey);
  PLHashEntry* he = *hep;
  if (nsnull != he) {
    return he->value;
  }
  return nsnull;
}

/**
 * Create an association between a frame and some data. This call
 * returns an old association if there was one (or nsnull if there
 * wasn't).
 */
void* FrameHashTable::Put(nsIFrame* aKey, void* aData)
{
  PRInt32 hashCode = (PRInt32) aKey;
  PLHashEntry** hep = PL_HashTableRawLookup(mTable, hashCode, aKey);
  PLHashEntry* he = *hep;
  if (nsnull != he) {
    void* oldValue = he->value;
    he->value = aData;
    return oldValue;
  }
  PL_HashTableRawAdd(mTable, hep, hashCode, aKey, aData);
  return nsnull;
}

/**
 * Remove an association between a frame and it's data. This returns
 * the old associated data.
 */
void* FrameHashTable::Remove(nsIFrame* aKey)
{
  PRInt32 hashCode = (PRInt32) aKey;
  PLHashEntry** hep = PL_HashTableRawLookup(mTable, hashCode, aKey);
  PLHashEntry* he = *hep;
  void* oldValue = nsnull;
  if (nsnull != he) {
    oldValue = he->value;
    PL_HashTableRawRemove(mTable, hep, he);
  }
  return oldValue;
}

//----------------------------------------------------------------------

static NS_DEFINE_IID(kISupportsIID, NS_ISUPPORTS_IID);
static NS_DEFINE_IID(kIPresShellIID, NS_IPRESSHELL_IID);
static NS_DEFINE_IID(kIDocumentObserverIID, NS_IDOCUMENTOBSERVER_IID);

class PresShell : public nsIPresShell {
public:
  PresShell();

  void* operator new(size_t sz) {
    void* rv = new char[sz];
    nsCRT::zero(rv, sz);
    return rv;
  }

  // nsISupports
  NS_DECL_ISUPPORTS

  // nsIDocumentObserver
  NS_IMETHOD SetTitle(const nsString& aTitle);
  virtual void BeginUpdate();
  virtual void EndUpdate();
  virtual void ContentChanged(nsIContent* aContent,
                              nsISubContent* aSubContent,
                              PRInt32 aChangeType);
  virtual void ContentAppended(nsIContent* aContainer);
  virtual void ContentInserted(nsIContent* aContainer,
                               nsIContent* aChild,
                               PRInt32 aIndexInContainer);
  virtual void ContentReplaced(nsIContent* aContainer,
                               nsIContent* aOldChild,
                               nsIContent* aNewChild,
                               PRInt32 aIndexInContainer);
  virtual void ContentWillBeRemoved(nsIContent* aContainer,
                                    nsIContent* aChild,
                                    PRInt32 aIndexInContainer);
  virtual void ContentHasBeenRemoved(nsIContent* aContainer,
                                     nsIContent* aChild,
                                     PRInt32 aIndexInContainer);
  virtual void StyleSheetAdded(nsIStyleSheet* aStyleSheet);

  // nsIPresShell
  virtual nsresult Init(nsIDocument* aDocument,
                        nsIPresContext* aPresContext,
                        nsIViewManager* aViewManager,
                        nsIStyleSet* aStyleSet);
  virtual nsIDocument* GetDocument();
  virtual nsIPresContext* GetPresContext();
  virtual nsIViewManager* GetViewManager();
  virtual nsIStyleSet* GetStyleSet();
  virtual void BeginObservingDocument();
  virtual void EndObservingDocument();
  virtual void ResizeReflow(nscoord aWidth, nscoord aHeight);
  virtual nsIFrame* GetRootFrame();
  virtual nsIFrame* FindFrameWithContent(nsIContent* aContent);
  virtual void AppendReflowCommand(nsReflowCommand* aReflowCommand);
  virtual void ProcessReflowCommands();
  virtual void PutCachedData(nsIFrame* aKeyFrame, void* aData);
  virtual void* GetCachedData(nsIFrame* aKeyFrame);
  virtual void* RemoveCachedData(nsIFrame* aKeyFrame);

protected:
  ~PresShell();

  FrameHashTable* mCache;
  nsIDocument* mDocument;
  nsIPresContext* mPresContext;
  nsIStyleSet* mStyleSet;
  nsIFrame* mRootFrame;
  nsIViewManager* mViewManager;
  PRUint32 mUpdateCount;
  nsVoidArray mReflowCommands;
};

//----------------------------------------------------------------------

PresShell::PresShell()
{
}

nsrefcnt PresShell::AddRef(void)
{
  return ++mRefCnt;
}

nsrefcnt PresShell::Release(void)
{
  NS_PRECONDITION(0 != mRefCnt, "bad refcnt");
  if (--mRefCnt == 0) {
    delete this;
    return 0;
  }
  return mRefCnt;
}

nsresult PresShell::QueryInterface(const nsIID& aIID, void** aInstancePtr)
{
  if (aIID.Equals(kIPresShellIID)) {
    *aInstancePtr = (void*) ((nsIPresShell*) this);
    AddRef();
    return NS_OK;
  }
  if (aIID.Equals(kIDocumentObserverIID)) {
    *aInstancePtr = (void*) ((nsIDocumentObserver*) this);
    AddRef();
    return NS_OK;
  }
  if (aIID.Equals(kISupportsIID)) {
    *aInstancePtr = (void*) ((nsISupports*) ((nsIPresShell*)this));
    AddRef();
    return NS_OK;
  }
  return NS_NOINTERFACE;
}

PresShell::~PresShell()
{
  delete mCache;

  if (nsnull != mDocument) {
    mDocument->DeleteShell(this);
    NS_RELEASE(mDocument);
  }
  if (nsnull != mRootFrame) {
    mRootFrame->DeleteFrame();
  }
  NS_IF_RELEASE(mPresContext);
  NS_IF_RELEASE(mViewManager);
  NS_IF_RELEASE(mStyleSet);
}

/**
 * Initialize the presentation shell. Create view manager and style
 * manager.
 */
nsresult PresShell::Init(nsIDocument* aDocument,
                         nsIPresContext* aPresContext,
                         nsIViewManager* aViewManager,
                         nsIStyleSet* aStyleSet)
{
  NS_PRECONDITION(nsnull != aDocument, "null ptr");
  NS_PRECONDITION(nsnull != aPresContext, "null ptr");
  NS_PRECONDITION(nsnull != aViewManager, "null ptr");
  if ((nsnull == aDocument) || (nsnull == aPresContext) ||
      (nsnull == aViewManager)) {
    return NS_ERROR_NULL_POINTER;
  }
  if (nsnull != mDocument) {
    return NS_ERROR_ALREADY_INITIALIZED;
  }

  mCache = new FrameHashTable();

  mDocument = aDocument;
  NS_ADDREF(aDocument);
  mViewManager = aViewManager;
  NS_ADDREF(mViewManager);

  // Bind the context to the presentation shell.
  mPresContext = aPresContext;
  NS_ADDREF(aPresContext);
  aPresContext->SetShell(this);

  mStyleSet = aStyleSet;
  NS_ADDREF(aStyleSet);

  return NS_OK;
}

nsIDocument* PresShell::GetDocument()
{
  NS_IF_ADDREF(mDocument);
  return mDocument;
}

nsIPresContext* PresShell::GetPresContext()
{
  NS_IF_ADDREF(mPresContext);
  return mPresContext;
}

nsIViewManager* PresShell::GetViewManager()
{
  NS_IF_ADDREF(mViewManager);
  return mViewManager;
}

nsIStyleSet* PresShell::GetStyleSet()
{
  NS_IF_ADDREF(mStyleSet);
  return mStyleSet;
}

// Make shell be a document observer
void PresShell::BeginObservingDocument()
{
  if (nsnull != mDocument) {
    mDocument->AddObserver(this);
  }
}

// Make shell stop being a document observer
void PresShell::EndObservingDocument()
{
  if (nsnull != mDocument) {
    mDocument->RemoveObserver(this);
  }
}

void PresShell::ResizeReflow(nscoord aWidth, nscoord aHeight)
{
  if (nsnull != mPresContext) {
    nsRect r(0, 0, aWidth, aHeight);
    mPresContext->SetVisibleArea(r);
  }

  if (nsnull == mRootFrame) {
    if (nsnull != mDocument) {
      nsIContent* root = mDocument->GetRootContent();
      if (nsnull != root) {
        nsIContentDelegate* cd = root->GetDelegate(mPresContext);
        if (nsnull != cd) {
          mRootFrame = cd->CreateFrame(mPresContext, root, -1, nsnull);
          NS_RELEASE(cd);

          // set root frame's style context
          nsIStyleContext* rootContext = mPresContext->ResolveStyleContextFor(root, nsnull);
          mRootFrame->SetStyleContext(rootContext);
          NS_RELEASE(rootContext);

          // Bind root frame to root view (and root window)
          nsIView* rootView = mViewManager->GetRootView();
          mRootFrame->SetView(rootView);
          NS_RELEASE(rootView);
        }
        NS_RELEASE(root);
      }
    }
  }

  if (nsnull != mRootFrame) {
    nsRect bounds = mPresContext->GetVisibleArea();
    nsReflowMetrics desiredSize;
    nsSize maxSize(bounds.width, bounds.height);

    // Kick off a top-down reflow
#ifdef NS_DEBUG
    mRootFrame->VerifyTree();
#endif
    nsIFrame::ReflowStatus  status;

    mRootFrame->ResizeReflow(mPresContext, desiredSize, maxSize, nsnull, status);
    mRootFrame->SizeTo(desiredSize.width, desiredSize.height);
#ifdef NS_DEBUG
    mRootFrame->VerifyTree();
#endif
#ifdef NOISY
    mRootFrame->List();
#endif

    //force a redraw of the entire view

#if 0
    nsIView *view = mRootFrame->GetView();

    if (nsnull != view) {
      nsRect rect = mRootFrame->GetRect();
      nsIViewManager* vm = view->GetViewManager();
      vm->UpdateView(view, &rect, NS_VMREFRESH_DOUBLE_BUFFER);
      NS_RELEASE(vm);
      NS_RELEASE(view);
    }
#endif
    // XXX if debugging then we should assert that the cache is empty
  } else {
#ifdef NOISY
    printf("PresShell::ResizeReflow: null root frame\n");
#endif
  }
}

nsIFrame* PresShell::GetRootFrame()
{
  return mRootFrame;
}

NS_IMETHODIMP PresShell::SetTitle(const nsString& aTitle)
{
  if (nsnull != mPresContext) {
    nsISupports* container;
    if (NS_OK == mPresContext->GetContainer(&container)) {
      if (nsnull != container) {
        nsIDocumentObserver* docob;
        if (NS_OK == container->QueryInterface(kIDocumentObserverIID,
                                               (void**) &docob)) {
          docob->SetTitle(aTitle);
          NS_RELEASE(docob);
        }
        NS_RELEASE(container);
      }
    }
  }
  return NS_OK;
}

void PresShell::BeginUpdate()
{
  mUpdateCount++;
}

void PresShell::EndUpdate()
{
  NS_PRECONDITION(0 != mUpdateCount, "too many EndUpdate's");
  if (--mUpdateCount == 0) {
    // XXX do something here
  }
}

void PresShell::AppendReflowCommand(nsReflowCommand* aReflowCommand)
{
  mReflowCommands.AppendElement(aReflowCommand);
}

void PresShell::ProcessReflowCommands()
{
  if (0 != mReflowCommands.Count()) {
    nsReflowMetrics desiredSize;

    while (0 != mReflowCommands.Count()) {
      nsReflowCommand* rc = (nsReflowCommand*) mReflowCommands.ElementAt(0);
      mReflowCommands.RemoveElementAt(0);

      // Dispatch the reflow command
      nsSize          maxSize;
      
      mRootFrame->GetSize(maxSize);
      rc->Dispatch(desiredSize, maxSize);
      delete rc;
    }

    // Place and size the root frame
    mRootFrame->SizeTo(desiredSize.width, desiredSize.height);
#ifdef NS_DEBUG
    mRootFrame->VerifyTree();
#endif
  }
}

void PresShell::ContentChanged(nsIContent* aContent,
                               nsISubContent* aSubContent,
                               PRInt32 aChangeType)
{
}

void PresShell::ContentAppended(nsIContent* aContainer)
{
  NS_PRECONDITION(nsnull != mRootFrame, "null root frame");
  nsReflowCommand* rc = new nsReflowCommand(mPresContext,
                                            FindFrameWithContent(aContainer),
                                            nsReflowCommand::ContentAppended,
                                            aContainer);
  AppendReflowCommand(rc);
  ProcessReflowCommands();
}

void PresShell::ContentInserted(nsIContent* aContainer,
                                nsIContent* aChild,
                                PRInt32 aIndexInContainer)
{
  NS_PRECONDITION(nsnull != mRootFrame, "null root frame");
  nsReflowCommand* rc = new nsReflowCommand(mPresContext,
                                            FindFrameWithContent(aContainer),
                                            nsReflowCommand::ContentInserted,
                                            aContainer, aChild,
                                            aIndexInContainer);
  AppendReflowCommand(rc);
  ProcessReflowCommands();
}

void PresShell::ContentReplaced(nsIContent* aContainer,
                                nsIContent* aOldChild,
                                nsIContent* aNewChild,
                                PRInt32 aIndexInContainer)
{
  NS_PRECONDITION(nsnull != mRootFrame, "null root frame");
  nsReflowCommand* rc = new nsReflowCommand(mPresContext,
                                            FindFrameWithContent(aContainer),
                                            nsReflowCommand::ContentReplaced,
                                            aContainer, aOldChild, aNewChild,
                                            aIndexInContainer);
  AppendReflowCommand(rc);
  ProcessReflowCommands();
}

// XXX keep this?
void PresShell::ContentWillBeRemoved(nsIContent* aContainer,
                                     nsIContent* aChild,
                                     PRInt32 aIndexInContainer)
{
}

void PresShell::ContentHasBeenRemoved(nsIContent* aContainer,
                                      nsIContent* aChild,
                                      PRInt32 aIndexInContainer)
{
  NS_PRECONDITION(nsnull != mRootFrame, "null root frame");
  nsReflowCommand* rc = new nsReflowCommand(mPresContext,
                                            FindFrameWithContent(aContainer),
                                            nsReflowCommand::ContentDeleted,
                                            aContainer, aChild,
                                            aIndexInContainer);
  AppendReflowCommand(rc);
  ProcessReflowCommands();
}

void PresShell::StyleSheetAdded(nsIStyleSheet* aStyleSheet)
{
}

static nsIFrame* FindFrameWithContent(nsIFrame* aFrame, nsIContent* aContent)
{
  nsIContent* frameContent;
   
  aFrame->GetContent(frameContent);
  if (frameContent == aContent) {
    NS_RELEASE(frameContent);
    return aFrame;
  }
  NS_RELEASE(frameContent);

  aFrame->FirstChild(aFrame);
  while (aFrame) {
    nsIFrame* result = FindFrameWithContent(aFrame, aContent);

    if (result) {
      return result;
    }

    aFrame->GetNextSibling(aFrame);
  }

  return nsnull;
}

nsIFrame* PresShell::FindFrameWithContent(nsIContent* aContent)
{
  // For the time being do a brute force depth-first search of
  // the frame tree
  return ::FindFrameWithContent(mRootFrame, aContent);
}

void PresShell::PutCachedData(nsIFrame* aKeyFrame, void* aData)
{
  void* oldData = mCache->Put(aKeyFrame, aData);
  NS_ASSERTION(nsnull == oldData, "bad cached data usage");
}

void* PresShell::GetCachedData(nsIFrame* aKeyFrame)
{
  return mCache->Get(aKeyFrame);
}

void* PresShell::RemoveCachedData(nsIFrame* aKeyFrame)
{
  return mCache->Remove(aKeyFrame);
}

//----------------------------------------------------------------------

NS_LAYOUT nsresult
NS_NewPresShell(nsIPresShell** aInstancePtrResult)
{
  NS_PRECONDITION(nsnull != aInstancePtrResult, "null ptr");
  if (nsnull == aInstancePtrResult) {
    return NS_ERROR_NULL_POINTER;
  }
  PresShell* it = new PresShell();
  if (nsnull == it) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  return it->QueryInterface(kIPresShellIID, (void **) aInstancePtrResult);
}
