/*
 * BEGIN FLOCK GPL
 *
 * Copyright Flock Inc. 2005-2009
 * http://flock.com
 *
 * This file may be used under the terms of the
 * GNU General Public License Version 2 or later (the "GPL"),
 * http://www.gnu.org/licenses/gpl.html
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * END FLOCK GPL
 */

#include "flockStreamUtils.h"

#include "nsIRDFService.h"
#include "nsIRDFContainerUtils.h"
#include "nsServiceManagerUtils.h"
#include "nsComponentManagerUtils.h"

#include "rdf.h"

#include "flockIRDFObserver.h"


enum {
  PROP_UNSEEN,
  PROP_UNSEEN_ITEMS,
  PROP_HAS_UNSEEN_ITEMS,
  PROP_COUNT
};


struct WatchProperty {
  PRUint16 type;
  const char *URN;
};

#define ARRAY_LENGTH(array_) (sizeof(array_) / sizeof((array_)[0]))

static const struct WatchProperty gWatchProperties[] = {
  { flockIRDFObserver::TYPE_ALL,    "http://flock.com/rdf#unseen"         },
  { flockIRDFObserver::TYPE_CHANGE, "http://flock.com/rdf#unseenItems"    },
  { 0,                              "http://flock.com/rdf#hasUnseenItems" },
  { flockIRDFObserver::TYPE_CHANGE, "http://flock.com/rdf#count"          },
};


NS_IMPL_ISUPPORTS4(flockStreamCountsPropagator, flockIStreamCountsPropagator,
                   flockIRDFObserver, nsIObserver, nsITimerCallback)


flockStreamCountsPropagator::flockStreamCountsPropagator()
{
}

flockStreamCountsPropagator::~flockStreamCountsPropagator()
{
}

NS_IMETHODIMP
flockStreamCountsPropagator::Initialize()
{
  nsresult rv;

  NS_ENSURE_TRUE(mStreamCounts.Init(128), NS_ERROR_OUT_OF_MEMORY);

  mRDFService = do_GetService("@mozilla.org/rdf/rdf-service;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  mRDFCU = do_GetService("@mozilla.org/rdf/container-utils;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  mObserverService = do_GetService("@mozilla.org/observer-service;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  mUpdateTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = mRDFService->GetDataSource("rdf:flock-favorites",
                                  getter_AddRefs(mFavesDS));
  NS_ENSURE_SUCCESS(rv, rv);

  mFLS = do_CreateInstance(FLOCK_RDFSOURCEUTILS_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<flockIRDFObservable> faves = do_QueryInterface(mFavesDS);

  for (PRUint32 i = 0; i < ARRAY_LENGTH(gWatchProperties); i++) {
    nsCOMPtr<nsIRDFResource> res;
    rv = mRDFService->GetResource(nsDependentCString(gWatchProperties[i].URN),
                                  getter_AddRefs(res));
    NS_ENSURE_SUCCESS(rv, rv);

    mProperties.AppendObject(res);

    if (gWatchProperties[i].type != 0) {
      rv = faves->AddArcObserver(gWatchProperties[i].type, NULL, res, NULL,
                                 this);
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  rv = faves->AddArcObserver(TYPE_ASSERT_PARENT, NULL, NULL, NULL, this);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = faves->AddArcObserver(TYPE_UNASSERT_PARENT, NULL, NULL, NULL, this);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

flockStreamCountsPropagator::StreamCounts *
flockStreamCountsPropagator::GetStreamCounts(nsIRDFResource *aResource)
{
  StreamCounts *counts;

  if (!mStreamCounts.Get(aResource, &counts)) {
    counts = new StreamCounts;
    mStreamCounts.Put(aResource, counts);
  }

  return counts;
}

nsresult
flockStreamCountsPropagator::GetIntFromLiteral(nsIRDFNode *aNode,
                                               PRInt32 *retval)
{
  nsresult rv;

  nsCOMPtr<nsIRDFLiteral> lit(do_QueryInterface(aNode, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  const PRUnichar *s;
  rv = lit->GetValueConst(&s);
  NS_ENSURE_SUCCESS(rv, rv);

  nsDependentString val(s);
  PRUint32 ec;
  *retval = val.ToInteger(&ec);
  return PRInt32(ec);
}

nsresult
flockStreamCountsPropagator::GetBooleanFromLiteral(nsIRDFNode *aNode,
                                                   PRBool *retval)
{
  nsresult rv;

  nsCOMPtr<nsIRDFLiteral> lit(do_QueryInterface(aNode));
  if (lit) {
    const PRUnichar *s;
    rv = lit->GetValueConst(&s);
    NS_ENSURE_SUCCESS(rv, rv);
 
    nsDependentString val(s);
    *retval = val.EqualsLiteral("true");
  } else {
    *retval = PR_FALSE;
  }

  return NS_OK;
}

nsresult
flockStreamCountsPropagator::GetCountValue(nsIRDFDataSource *aDataSource,
                                           nsIRDFResource *aResource,
                                           int aPropIndex,
                                           PRInt32 *retval)
{
  nsresult rv;

  nsCOMPtr<nsIRDFNode> target;
  rv = aDataSource->GetTarget(aResource, mProperties[aPropIndex], PR_TRUE,
                              getter_AddRefs(target));
  NS_ENSURE_SUCCESS(rv, rv);

  if (rv != NS_RDF_NO_VALUE) {
    rv = GetIntFromLiteral(target, retval);
    if (NS_SUCCEEDED(rv)) return NS_OK;
  }

  *retval = 0;
  return NS_RDF_NO_VALUE;
}

nsresult
flockStreamCountsPropagator::GetBooleanValue(nsIRDFDataSource *aDataSource,
                                             nsIRDFResource *aResource,
                                             int aPropIndex,
                                             PRBool *retval)
{
  nsresult rv;

  nsCOMPtr<nsIRDFNode> target;
  rv = aDataSource->GetTarget(aResource, mProperties[aPropIndex], PR_TRUE,
                              getter_AddRefs(target));
  NS_ENSURE_SUCCESS(rv, rv);

  if (rv != NS_RDF_NO_VALUE) {
    rv = GetBooleanFromLiteral(target, retval);
    if (NS_SUCCEEDED(rv)) return NS_OK;
  }

  *retval = PR_FALSE;
  return NS_RDF_NO_VALUE;
}

nsresult
flockStreamCountsPropagator::SetCountValue(nsIRDFDataSource *aDataSource,
                                           nsIRDFResource *aResource,
                                           int aPropIndex,
                                           PRInt32 count)
{
  nsAutoString val;
  val.AppendInt(count);
  mFLS->Init(aDataSource, aResource);
  nsDependentCString prop(gWatchProperties[aPropIndex].URN);
  return mFLS->SetLitTarget(prop, val);
}

nsresult
flockStreamCountsPropagator::SetBooleanValue(nsIRDFDataSource *aDataSource,
                                              nsIRDFResource *aResource,
                                              int aPropIndex,
                                              PRBool value)
{
  mFLS->Init(aDataSource, aResource);
  nsDependentCString prop(gWatchProperties[aPropIndex].URN);
  return mFLS->SetLitTarget(prop,
                value ? NS_LITERAL_STRING("true"): NS_LITERAL_STRING("false"));
}

nsresult
flockStreamCountsPropagator::SetHasUnseenItems(nsIRDFDataSource *aDataSource,
                                               nsIRDFResource *aSource,
                                               PRInt32 unseenCount)
{
  nsresult rv;

  PRBool hasUnseenItems;
  rv = GetBooleanValue(aDataSource, aSource, PROP_HAS_UNSEEN_ITEMS, 
                       &hasUnseenItems);
  NS_ENSURE_SUCCESS(rv, rv);

  if (unseenCount > 0 && !hasUnseenItems)
    return SetBooleanValue(aDataSource, aSource, PROP_HAS_UNSEEN_ITEMS, true);
  else if (unseenCount == 0 && hasUnseenItems)
    return SetBooleanValue(aDataSource, aSource, PROP_HAS_UNSEEN_ITEMS, false);

  return NS_OK;
}

nsresult
flockStreamCountsPropagator::ProcessAssertParent(nsIRDFDataSource *aDataSource,
                                                 nsIRDFResource *aSource,
                                                 nsIRDFNode *aTarget,
                                                 PRBool aTruthValue)
{
  nsresult rv;

  nsCOMPtr<nsIRDFResource> child(do_QueryInterface(aTarget));
  if (child) {
    PRBool didUpdate = PR_FALSE;

    StreamCounts *counts = GetStreamCounts(aSource);

    PRInt32 unseenChildCount;
    rv = GetCountValue(aDataSource, child, PROP_UNSEEN_ITEMS,
                       &unseenChildCount);
    NS_ENSURE_SUCCESS(rv, rv);

    if (rv == NS_RDF_NO_VALUE) {
      PRBool unseen;
      rv = GetBooleanValue(aDataSource, child, PROP_UNSEEN, &unseen);
      NS_ENSURE_SUCCESS(rv, rv);

      if (unseen)
        unseenChildCount = 1;
    }

    if (unseenChildCount != 0) {
      didUpdate = PR_TRUE;

      if (aTruthValue)
        counts->unseen += unseenChildCount;
      else
        counts->unseen -= unseenChildCount;
    }

    PRInt32 totalChildCount;
    rv = GetCountValue(aDataSource, child, PROP_COUNT, &totalChildCount);
    NS_ENSURE_SUCCESS(rv, rv);

    if (rv == NS_RDF_NO_VALUE) {
      totalChildCount = 1;
    }

    if (totalChildCount != 0) {
      didUpdate = PR_TRUE;

      if (aTruthValue)
        counts->total += totalChildCount;
      else
        counts->total -= totalChildCount;
    }

    if (didUpdate)
      mUpdateTimer->InitWithCallback(this, 0, nsITimer::TYPE_ONE_SHOT);
  }

  return NS_OK;
}

nsresult
flockStreamCountsPropagator::GetParents(nsIRDFDataSource *aDataSource,
                                        nsIRDFResource *aSource,
                                        nsCOMArray<nsIRDFResource> &aArray)
{
  nsCOMPtr<nsISimpleEnumerator> arcsIn;
  aDataSource->ArcLabelsIn(aSource, getter_AddRefs(arcsIn));

  if (!arcsIn)
    return NS_OK;

  while (1) {
    PRBool hasMoreArcs = PR_FALSE;
    arcsIn->HasMoreElements(&hasMoreArcs);
    if (!hasMoreArcs)
      break;

    nsCOMPtr<nsISupports> isupports;
    arcsIn->GetNext(getter_AddRefs(isupports));
    if (!isupports)
      break;

    nsCOMPtr<nsIRDFResource> prop = do_QueryInterface(isupports);
    if (!prop)
      continue;

    PRBool isOrdinal;
    mRDFCU->IsOrdinalProperty(prop, &isOrdinal);
    if (!isOrdinal)
      continue;

    nsCOMPtr<nsISimpleEnumerator> sources;
    aDataSource->GetSources(prop, aSource, PR_TRUE, getter_AddRefs(sources));
    if (!sources)
      continue;

    while (1) {
      PRBool hasMoreSources = PR_FALSE;
      sources->HasMoreElements(&hasMoreSources);
      if (!hasMoreSources)
        break;

      nsCOMPtr<nsISupports> isupports2;
      sources->GetNext(getter_AddRefs(isupports2));
      if (!isupports2)
        break;

      nsCOMPtr<nsIRDFResource> resource = do_QueryInterface(isupports2);
      aArray.AppendObject(resource);
    }
  }

  return NS_OK;
}

nsresult
flockStreamCountsPropagator::ProcessUnseen(nsIRDFDataSource *aDataSource,
                                           PRUint16 aType,
                                           nsIRDFResource *aSource,
                                           nsIRDFNode *aTarget,
                                           nsIRDFNode *aOldTarget)
{
  nsresult rv;

  int countDirection;

  PRBool val;
  rv = GetBooleanFromLiteral(aTarget, &val);
  NS_ENSURE_SUCCESS(rv, rv);

  switch (aType) {
    case TYPE_ASSERT:
    case TYPE_UNASSERT:
      if (val) {
        countDirection = (aType == TYPE_ASSERT) ? 1 : -1;
      } else {
        // unseen is/was false, nothing needs to change
        return NS_OK;
      }

      break;

    case TYPE_CHANGE:
      PRBool oldVal;
      rv = GetBooleanFromLiteral(aOldTarget, &oldVal);
      NS_ENSURE_SUCCESS(rv, rv);

      if (oldVal != val) {
        countDirection = val ? 1 : -1;
      } else {
        // no effective change
        return NS_OK;
      }

      break;

    default:
      NS_NOTREACHED("unhandled event type");
      return NS_ERROR_UNEXPECTED;
  }

  nsCOMArray<nsIRDFResource> parents;
  GetParents(aDataSource, aSource, parents);

  if (parents.Count() > 0) {
    for (int i = 0; i < parents.Count(); i++) {
      StreamCounts *counts = GetStreamCounts(parents[i]);
      counts->unseen += countDirection;
    }

    mUpdateTimer->InitWithCallback(this, 0, nsITimer::TYPE_ONE_SHOT);
  }

  return NS_OK;
}

nsresult
flockStreamCountsPropagator::ProcessUnseenItems(nsIRDFDataSource *aDataSource,
                                                PRUint16 aType,
                                                nsIRDFResource *aSource,
                                                nsIRDFNode *aTarget,
                                                nsIRDFNode *aOldTarget)
{
  nsresult rv;

  PRInt32 val, oldVal;

  rv = GetIntFromLiteral(aTarget, &val);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = GetIntFromLiteral(aOldTarget, &oldVal);
  NS_ENSURE_SUCCESS(rv, rv);

  PRInt32 diff = val - oldVal;

  if (diff == 0)
    return NS_OK;

  nsCOMArray<nsIRDFResource> parents;
  GetParents(aDataSource, aSource, parents);

  if (parents.Count() > 0) {
    for (int i = 0; i < parents.Count(); i++) {
      StreamCounts *counts = GetStreamCounts(parents[i]);
      counts->unseen += diff;
    }

    mUpdateTimer->InitWithCallback(this, 0, nsITimer::TYPE_ONE_SHOT);
  }

  return NS_OK;
}

nsresult
flockStreamCountsPropagator::ProcessCount(nsIRDFDataSource *aDataSource,
                                          PRUint16 aType,
                                          nsIRDFResource *aSource,
                                          nsIRDFNode *aTarget,
                                          nsIRDFNode *aOldTarget)
{
  nsresult rv;

  PRInt32 val, oldVal;

  rv = GetIntFromLiteral(aTarget, &val);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = GetIntFromLiteral(aOldTarget, &oldVal);
  NS_ENSURE_SUCCESS(rv, rv);

  PRInt32 diff = val - oldVal;

  if (diff == 0)
    return NS_OK;

  nsCOMArray<nsIRDFResource> parents;
  GetParents(aDataSource, aSource, parents);

  if (parents.Count() > 0) {
    for (int i = 0; i < parents.Count(); i++) {
      StreamCounts *counts = GetStreamCounts(parents[i]);
      counts->total += diff;
    }

    mUpdateTimer->InitWithCallback(this, 0, nsITimer::TYPE_ONE_SHOT);
  }

  return NS_OK;
}

NS_IMETHODIMP
flockStreamCountsPropagator::RdfChanged(nsIRDFDataSource *aDataSource,
                                        PRUint16 aType,
                                        nsIRDFResource *aSource,
                                        nsIRDFResource *aPredicate,
                                        nsIRDFNode *aTarget,
                                        nsIRDFNode *aOldTarget)
{
  nsresult rv;

  NS_ENSURE_ARG_POINTER(aDataSource);
  NS_ENSURE_ARG_POINTER(aSource);
  NS_ENSURE_ARG_POINTER(aPredicate);

  if (aType == TYPE_ASSERT_PARENT)
    return ProcessAssertParent(aDataSource, aSource, aTarget, PR_TRUE);
  else if (aType == TYPE_UNASSERT_PARENT)
    return ProcessAssertParent(aDataSource, aSource, aTarget, PR_FALSE);

  PRBool nodeEqual;

  rv = aPredicate->EqualsNode(mProperties[PROP_UNSEEN], &nodeEqual);
  NS_ENSURE_SUCCESS(rv, rv);

  if (nodeEqual)
    return ProcessUnseen(aDataSource, aType, aSource, aTarget, aOldTarget);

  rv = aPredicate->EqualsNode(mProperties[PROP_UNSEEN_ITEMS], &nodeEqual);
  NS_ENSURE_SUCCESS(rv, rv);

  if (nodeEqual)
    return ProcessUnseenItems(aDataSource, aType, aSource, aTarget, aOldTarget);

  rv = aPredicate->EqualsNode(mProperties[PROP_COUNT], &nodeEqual);
  NS_ENSURE_SUCCESS(rv, rv);

  if (nodeEqual)
    return ProcessCount(aDataSource, aType, aSource, aTarget, aOldTarget);

  return NS_OK;
}

void
flockStreamCountsPropagator::UpdateCounts(CountDelta *delta)
{
  nsresult rv;
  PRBool didUpdate = PR_FALSE;

  if (delta->counts.unseen != 0) {
    PRInt32 unseenCount;

    rv = GetCountValue(mFavesDS, delta->resource, PROP_UNSEEN_ITEMS,
                       &unseenCount);
    if (rv != NS_RDF_NO_VALUE) {
      unseenCount += delta->counts.unseen;

      // fix up old corrupted profiles
      if (unseenCount < 0) {
        unseenCount = 0;
      }

      SetCountValue(mFavesDS, delta->resource, PROP_UNSEEN_ITEMS,
                    unseenCount);

      SetHasUnseenItems(mFavesDS, delta->resource, unseenCount);

      didUpdate = PR_TRUE;
    }
  }

  if (delta->counts.total != 0) {
    PRInt32 totalCount;

    rv = GetCountValue(mFavesDS, delta->resource, PROP_COUNT, &totalCount);
    if (rv != NS_RDF_NO_VALUE) {
      totalCount += delta->counts.total;

      // fix up old corrupted profiles
      if (totalCount < 0) {
        totalCount = 0;
      }

      SetCountValue(mFavesDS, delta->resource, PROP_COUNT, totalCount);

      didUpdate = PR_TRUE;
    }
  }

  if (didUpdate) {
    const char *uri;
    nsresult rv = delta->resource->GetValueConst(&uri);
    if (NS_SUCCEEDED(rv)) {
      mObserverService->NotifyObservers(NULL, "notificationCountChanged",
                                        NS_ConvertUTF8toUTF16(uri).get());
    }
  }
}

PLDHashOperator PR_CALLBACK
flockStreamCountsPropagator::CollectCounts(nsISupports *aKey,
                                          StreamCounts *aCounts,
                                          void *aUserData)
{
  nsVoidArray *array = static_cast<nsVoidArray *>(aUserData);

  CountDelta *delta = new CountDelta;

  delta->resource = do_QueryInterface(aKey);
  delta->counts = *aCounts;

  array->AppendElement(delta);
  return PL_DHASH_NEXT;
}

NS_IMETHODIMP
flockStreamCountsPropagator::Observe(nsISupports *aSubject,
                                     const char *aTopic,
                                     const PRUnichar *data)
{
  return NS_OK;
}

NS_IMETHODIMP
flockStreamCountsPropagator::Notify(nsITimer *aTimer)
{
  nsVoidArray resources(mStreamCounts.Count());
  mStreamCounts.EnumerateRead(CollectCounts, &resources);
  mStreamCounts.Clear();

  for (int i = 0; i < resources.Count(); i++) {
    CountDelta *delta = static_cast<CountDelta *>(resources.FastElementAt(i));
    UpdateCounts(delta);
    delete delta;
  }

  return NS_OK;
}

NS_IMETHODIMP
flockStreamCountsPropagator::SyncCounts(nsIRDFResource *aResource)
{
  StreamCounts *counts;

  if (mStreamCounts.Get(aResource, &counts)) {
    CountDelta delta;

    delta.resource = aResource;
    delta.counts = *counts;

    mStreamCounts.Remove(aResource);

    UpdateCounts(&delta);
  }

  return NS_OK;
}

static flockStreamCountsPropagator *gSingleton = NULL;

flockStreamCountsPropagator *
flockStreamCountsPropagator::GetSingleton()
{
  if (!gSingleton) {
    gSingleton = new flockStreamCountsPropagator();
    if (gSingleton) {
      NS_ADDREF(gSingleton);
      if (NS_FAILED(gSingleton->Initialize())) {
        NS_RELEASE(gSingleton);
      }
    }
  }
  return gSingleton;
}
